From 648f1749e9cddaec1be0c11958d63bea76474de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 21 Nov 2025 14:48:54 +0100 Subject: [PATCH] feat(core): Add more details to MCP errors and telemetry (no-changelog) (#22118) --- .../__tests__/mcp-oauth-token.service.test.ts | 12 +-- .../mcp-server-middleware.service.test.ts | 88 +++++++++++++------ .../src/modules/mcp/mcp-api-key.service.ts | 31 +++++-- .../modules/mcp/mcp-oauth-token.service.ts | 39 ++++++-- .../mcp/mcp-server-middleware.service.ts | 70 ++++++++++----- packages/cli/src/modules/mcp/mcp.errors.ts | 16 ++++ packages/cli/src/modules/mcp/mcp.types.ts | 24 +++++ .../components/MCPConnectionInstructions.vue | 25 +++++- .../AccessTokenConnectionInstructions.vue | 21 ++++- .../ConnectionParameter.vue | 11 ++- .../OAuthConnectionInstructions.vue | 7 +- 11 files changed, 270 insertions(+), 74 deletions(-) diff --git a/packages/cli/src/modules/mcp/__tests__/mcp-oauth-token.service.test.ts b/packages/cli/src/modules/mcp/__tests__/mcp-oauth-token.service.test.ts index 66d62b71b48..c76e5e4d53c 100644 --- a/packages/cli/src/modules/mcp/__tests__/mcp-oauth-token.service.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/mcp-oauth-token.service.test.ts @@ -213,7 +213,7 @@ describe('McpOAuthTokenService', () => { const invalidToken = 'invalid.jwt.token'; await expect(service.verifyAccessToken(invalidToken)).rejects.toThrow( - 'Invalid access token: JWT verification failed', + 'JWT Verification Failed', ); }); @@ -225,7 +225,7 @@ describe('McpOAuthTokenService', () => { }); await expect(service.verifyAccessToken(wrongAudienceToken)).rejects.toThrow( - 'Invalid access token: JWT verification failed', + 'JWT Verification Failed', ); }); @@ -237,7 +237,7 @@ describe('McpOAuthTokenService', () => { accessTokenRepository.findOne.mockResolvedValue(null); await expect(service.verifyAccessToken(accessToken)).rejects.toThrow( - 'Invalid access token: not found in database', + 'Access Token Not Found in Database', ); }); }); @@ -261,7 +261,7 @@ describe('McpOAuthTokenService', () => { const result = await service.verifyOAuthAccessToken(accessToken); - expect(result).toEqual(user); + expect(result).toEqual({ user }); expect(userRepository.findOne).toHaveBeenCalledWith({ where: { id: userId }, relations: ['role'], @@ -273,7 +273,7 @@ describe('McpOAuthTokenService', () => { const result = await service.verifyOAuthAccessToken(invalidToken); - expect(result).toBeNull(); + expect(result).toMatchObject({ user: null }); }); it('should return null when user not found', async () => { @@ -292,7 +292,7 @@ describe('McpOAuthTokenService', () => { const result = await service.verifyOAuthAccessToken(accessToken); - expect(result).toBeNull(); + expect(result).toMatchObject({ user: null }); }); }); diff --git a/packages/cli/src/modules/mcp/__tests__/mcp-server-middleware.service.test.ts b/packages/cli/src/modules/mcp/__tests__/mcp-server-middleware.service.test.ts index 230e6cf97b4..efc94df6c0c 100644 --- a/packages/cli/src/modules/mcp/__tests__/mcp-server-middleware.service.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/mcp-server-middleware.service.test.ts @@ -58,11 +58,11 @@ describe('McpServerMiddlewareService', () => { meta: { isOAuth: true }, }); - oauthTokenService.verifyOAuthAccessToken.mockResolvedValue(user); + oauthTokenService.verifyOAuthAccessToken.mockResolvedValue({ user }); const result = await service.getUserForToken(oauthToken); - expect(result).toEqual(user); + expect(result).toEqual({ user }); expect(oauthTokenService.verifyOAuthAccessToken).toHaveBeenCalledWith(oauthToken); expect(mcpServerApiKeyService.verifyApiKey).not.toHaveBeenCalled(); }); @@ -74,11 +74,11 @@ describe('McpServerMiddlewareService', () => { aud: 'mcp-server-api', }); - mcpServerApiKeyService.verifyApiKey.mockResolvedValue(user); + mcpServerApiKeyService.verifyApiKey.mockResolvedValue({ user }); const result = await service.getUserForToken(apiKeyToken); - expect(result).toEqual(user); + expect(result).toEqual({ user }); expect(mcpServerApiKeyService.verifyApiKey).toHaveBeenCalledWith(apiKeyToken); expect(oauthTokenService.verifyOAuthAccessToken).not.toHaveBeenCalled(); }); @@ -91,11 +91,11 @@ describe('McpServerMiddlewareService', () => { meta: { isOAuth: false }, }); - mcpServerApiKeyService.verifyApiKey.mockResolvedValue(user); + mcpServerApiKeyService.verifyApiKey.mockResolvedValue({ user }); const result = await service.getUserForToken(apiKeyToken); - expect(result).toEqual(user); + expect(result).toEqual({ user }); expect(mcpServerApiKeyService.verifyApiKey).toHaveBeenCalledWith(apiKeyToken); expect(oauthTokenService.verifyOAuthAccessToken).not.toHaveBeenCalled(); }); @@ -103,10 +103,10 @@ describe('McpServerMiddlewareService', () => { it('should return null for invalid JWT format', async () => { const invalidToken = 'not-a-jwt-token'; - mcpServerApiKeyService.verifyApiKey.mockResolvedValue(null); + mcpServerApiKeyService.verifyApiKey.mockResolvedValue({ user: null }); const result = await service.getUserForToken(invalidToken); - expect(result).toBeNull(); + expect(result).toMatchObject({ user: null }); expect(oauthTokenService.verifyOAuthAccessToken).not.toHaveBeenCalled(); }); @@ -117,11 +117,11 @@ describe('McpServerMiddlewareService', () => { meta: { isOAuth: true }, }); - oauthTokenService.verifyOAuthAccessToken.mockResolvedValue(null); + oauthTokenService.verifyOAuthAccessToken.mockResolvedValue({ user: null }); const result = await service.getUserForToken(oauthToken); - expect(result).toBeNull(); + expect(result).toMatchObject({ user: null }); }); it('should return null when API key verification fails', async () => { @@ -130,11 +130,11 @@ describe('McpServerMiddlewareService', () => { aud: 'mcp-server-api', }); - mcpServerApiKeyService.verifyApiKey.mockResolvedValue(null); + mcpServerApiKeyService.verifyApiKey.mockResolvedValue({ user: null }); const result = await service.getUserForToken(apiKeyToken); - expect(result).toBeNull(); + expect(result).toMatchObject({ user: null }); }); }); @@ -153,17 +153,22 @@ describe('McpServerMiddlewareService', () => { expect(res.header).toHaveBeenCalledWith('WWW-Authenticate', 'Bearer realm="n8n MCP Server"'); expect(res.status).toHaveBeenCalledWith(401); - expect(res.send).toHaveBeenCalledWith({ message: 'Unauthorized' }); + expect(res.send).toHaveBeenCalledWith({ + message: 'Unauthorized: Authorization header not sent', + }); expect(next).not.toHaveBeenCalled(); expect(telemetry.track).toHaveBeenCalledWith('User connected to MCP server', { mcp_connection_status: 'error', error: 'Unauthorized', client_name: undefined, client_version: undefined, + auth_type: 'unknown', + error_details: 'Authorization header not sent', + reason: 'missing_authorization_header', }); }); - it('should throw error when authorization header does not start with Bearer', async () => { + it('should return 401 with WWW-Authenticate header when authorization header does not start with Bearer', async () => { const req = mockReqWith('Basic sometoken'); const res = mockDeep(); res.status.mockReturnThis(); @@ -171,13 +176,26 @@ describe('McpServerMiddlewareService', () => { const middleware = service.getAuthMiddleware(); - await expect(middleware(req, res, next)).rejects.toThrow( - 'Invalid authorization header format', - ); + await middleware(req, res, next); + + expect(res.header).toHaveBeenCalledWith('WWW-Authenticate', 'Bearer realm="n8n MCP Server"'); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.send).toHaveBeenCalledWith({ + message: 'Unauthorized: Invalid authorization header format - Missing Bearer prefix', + }); expect(next).not.toHaveBeenCalled(); + expect(telemetry.track).toHaveBeenCalledWith('User connected to MCP server', { + mcp_connection_status: 'error', + error: 'Unauthorized', + client_name: undefined, + client_version: undefined, + auth_type: 'unknown', + error_details: 'Invalid authorization header format - Missing Bearer prefix', + reason: 'invalid_bearer_format', + }); }); - it('should throw error when Bearer token is malformed', async () => { + it('should return 401 with WWW-Authenticate header when Bearer token is malformed', async () => { const req = mockReqWith('Bearer'); const res = mockDeep(); res.status.mockReturnThis(); @@ -185,10 +203,23 @@ describe('McpServerMiddlewareService', () => { const middleware = service.getAuthMiddleware(); - await expect(middleware(req, res, next)).rejects.toThrow( - 'Invalid authorization header format', - ); + await middleware(req, res, next); + + expect(res.header).toHaveBeenCalledWith('WWW-Authenticate', 'Bearer realm="n8n MCP Server"'); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.send).toHaveBeenCalledWith({ + message: 'Unauthorized: Invalid authorization header format - Malformed Bearer token', + }); expect(next).not.toHaveBeenCalled(); + expect(telemetry.track).toHaveBeenCalledWith('User connected to MCP server', { + mcp_connection_status: 'error', + error: 'Unauthorized', + client_name: undefined, + client_version: undefined, + auth_type: 'unknown', + error_details: 'Invalid authorization header format - Malformed Bearer token', + reason: 'invalid_bearer_format', + }); }); it('should authenticate with valid OAuth token and call next', async () => { @@ -203,7 +234,7 @@ describe('McpServerMiddlewareService', () => { const res = mockDeep(); const next = jest.fn() as NextFunction; - oauthTokenService.verifyOAuthAccessToken.mockResolvedValue(user); + oauthTokenService.verifyOAuthAccessToken.mockResolvedValue({ user }); const middleware = service.getAuthMiddleware(); @@ -225,7 +256,7 @@ describe('McpServerMiddlewareService', () => { const res = mockDeep(); const next = jest.fn() as NextFunction; - mcpServerApiKeyService.verifyApiKey.mockResolvedValue(user); + mcpServerApiKeyService.verifyApiKey.mockResolvedValue({ user }); const middleware = service.getAuthMiddleware(); @@ -250,7 +281,7 @@ describe('McpServerMiddlewareService', () => { res.header.mockReturnThis(); const next = jest.fn() as NextFunction; - oauthTokenService.verifyOAuthAccessToken.mockResolvedValue(null); + oauthTokenService.verifyOAuthAccessToken.mockResolvedValue({ user: null }); const middleware = service.getAuthMiddleware(); @@ -282,11 +313,18 @@ describe('McpServerMiddlewareService', () => { await middleware(req, res, next); expect(res.header).toHaveBeenCalledWith('WWW-Authenticate', 'Bearer realm="n8n MCP Server"'); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.send).toHaveBeenCalledWith({ + message: 'Unauthorized: Authorization header not sent', + }); expect(telemetry.track).toHaveBeenCalledWith('User connected to MCP server', { mcp_connection_status: 'error', error: 'Unauthorized', client_name: 'test-client', client_version: '1.0.0', + auth_type: 'unknown', + error_details: 'Authorization header not sent', + reason: 'missing_authorization_header', }); }); @@ -298,7 +336,7 @@ describe('McpServerMiddlewareService', () => { res.header.mockReturnThis(); const next = jest.fn() as NextFunction; - mcpServerApiKeyService.verifyApiKey.mockResolvedValue(null); + mcpServerApiKeyService.verifyApiKey.mockResolvedValue({ user: null }); const middleware = service.getAuthMiddleware(); diff --git a/packages/cli/src/modules/mcp/mcp-api-key.service.ts b/packages/cli/src/modules/mcp/mcp-api-key.service.ts index 55719b2ea55..34014a95f4b 100644 --- a/packages/cli/src/modules/mcp/mcp-api-key.service.ts +++ b/packages/cli/src/modules/mcp/mcp-api-key.service.ts @@ -2,11 +2,12 @@ import { ApiKey, ApiKeyRepository, User, UserRepository } from '@n8n/db'; import { Service } from '@n8n/di'; import { EntityManager } from '@n8n/typeorm'; import { randomUUID } from 'crypto'; -import { ApiKeyAudience } from 'n8n-workflow'; - -import { JwtService } from '@/services/jwt.service'; +import { ApiKeyAudience, ensureError } from 'n8n-workflow'; import { AccessTokenRepository } from './database/repositories/oauth-access-token.repository'; +import { UserWithContext } from './mcp.types'; + +import { JwtService } from '@/services/jwt.service'; const API_KEY_AUDIENCE: ApiKeyAudience = 'mcp-server-api'; const API_KEY_ISSUER = 'n8n'; @@ -77,16 +78,34 @@ export class McpServerApiKeyService { }); } - async verifyApiKey(apiKey: string): Promise { + async verifyApiKey(apiKey: string): Promise { try { this.jwtService.verify(apiKey, { issuer: API_KEY_ISSUER, audience: API_KEY_AUDIENCE, }); - return await this.getUserForApiKey(apiKey); + const user = await this.getUserForApiKey(apiKey); + if (!user) { + return { + user: null, + context: { + reason: 'user_not_found', + auth_type: 'api_key', + }, + }; + } + return { user }; } catch (error) { - return null; + const errorForSure = ensureError(error); + return { + user: null, + context: { + reason: errorForSure.name === 'JsonWebTokenError' ? 'invalid_token' : 'unknown_error', + auth_type: 'api_key', + error_details: errorForSure.message, + }, + }; } } diff --git a/packages/cli/src/modules/mcp/mcp-oauth-token.service.ts b/packages/cli/src/modules/mcp/mcp-oauth-token.service.ts index 560e8256b33..8cc66cd653c 100644 --- a/packages/cli/src/modules/mcp/mcp-oauth-token.service.ts +++ b/packages/cli/src/modules/mcp/mcp-oauth-token.service.ts @@ -2,17 +2,20 @@ import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types'; import { OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth'; import { Logger } from '@n8n/backend-common'; import { Time } from '@n8n/constants'; -import { User, UserRepository, withTransaction } from '@n8n/db'; +import { UserRepository, withTransaction } from '@n8n/db'; import { Service } from '@n8n/di'; import { MoreThanOrEqual } from '@n8n/typeorm'; +import { ensureError } from 'n8n-workflow'; import { randomBytes, randomUUID } from 'node:crypto'; -import { JwtService } from '@/services/jwt.service'; - import { AccessToken } from './database/entities/oauth-access-token.entity'; import { RefreshToken } from './database/entities/oauth-refresh-token.entity'; import { AccessTokenRepository } from './database/repositories/oauth-access-token.repository'; import { RefreshTokenRepository } from './database/repositories/oauth-refresh-token.repository'; +import { AccessTokenNotFoundError, JWTVerificationError } from './mcp.errors'; +import { UserWithContext } from './mcp.types'; + +import { JwtService } from '@/services/jwt.service'; /** * Manages OAuth 2.1 token lifecycle for MCP server @@ -142,7 +145,7 @@ export class McpOAuthTokenService { try { decoded = this.jwtService.verify(token, { audience: this.MCP_AUDIENCE }); } catch (error) { - throw new Error('Invalid access token: JWT verification failed'); + throw new JWTVerificationError(); } const accessTokenRecord = await this.accessTokenRepository.findOne({ @@ -150,7 +153,7 @@ export class McpOAuthTokenService { }); if (!accessTokenRecord) { - throw new Error('Invalid access token: not found in database'); + throw new AccessTokenNotFoundError(); } return { @@ -163,13 +166,13 @@ export class McpOAuthTokenService { }; } - async verifyOAuthAccessToken(token: string): Promise { + async verifyOAuthAccessToken(token: string): Promise { try { const authInfo = await this.verifyAccessToken(token); const userId = authInfo.extra?.userId as string; if (!userId) { - return null; + return { user: null, context: { reason: 'user_id_not_in_auth_info', auth_type: 'oauth' } }; } const user = await this.userRepository.findOne({ @@ -177,9 +180,27 @@ export class McpOAuthTokenService { relations: ['role'], }); - return user; + if (!user) { + return { user: null, context: { reason: 'user_not_found', auth_type: 'oauth' } }; + } + + return { user }; } catch (error) { - return null; + const errorForSure = ensureError(error); + const reason = + errorForSure instanceof JWTVerificationError + ? 'invalid_token' + : errorForSure instanceof AccessTokenNotFoundError + ? 'token_not_found_in_db' + : 'unknown_error'; + return { + user: null, + context: { + reason, + auth_type: 'oauth', + error_details: errorForSure.message, + }, + }; } } diff --git a/packages/cli/src/modules/mcp/mcp-server-middleware.service.ts b/packages/cli/src/modules/mcp/mcp-server-middleware.service.ts index 944d8f07bc7..c5fd72ae073 100644 --- a/packages/cli/src/modules/mcp/mcp-server-middleware.service.ts +++ b/packages/cli/src/modules/mcp/mcp-server-middleware.service.ts @@ -1,16 +1,18 @@ -import { AuthenticatedRequest, User } from '@n8n/db'; +import { AuthenticatedRequest } from '@n8n/db'; import { Service } from '@n8n/di'; import { NextFunction, Response, Request } from 'express'; - -import { AuthError } from '@/errors/response-errors/auth.error'; -import { JwtService } from '@/services/jwt.service'; -import { Telemetry } from '@/telemetry'; +import { ensureError } from 'n8n-workflow'; import { McpServerApiKeyService } from './mcp-api-key.service'; import { McpOAuthTokenService } from './mcp-oauth-token.service'; import { USER_CONNECTED_TO_MCP_EVENT, UNAUTHORIZED_ERROR_MESSAGE } from './mcp.constants'; +import type { TelemetryAuthContext, UserWithContext } from './mcp.types'; import { getClientInfo } from './mcp.utils'; +import { AuthError } from '@/errors/response-errors/auth.error'; +import { JwtService } from '@/services/jwt.service'; +import { Telemetry } from '@/telemetry'; + /** * MCP Server Middleware Service * Centralizes authentication for MCP server endpoints @@ -29,12 +31,19 @@ export class McpServerMiddlewareService { * Get user for a given token (API key or OAuth access token) * Uses JWT metadata to determine token type and route to correct validation */ - async getUserForToken(token: string): Promise { + async getUserForToken(token: string): Promise { let decoded: { meta?: { isOAuth?: boolean } }; try { decoded = this.jwtService.decode<{ meta?: { isOAuth?: boolean } }>(token); } catch (error) { - return null; + return { + user: null, + context: { + reason: 'jwt_decode_failed', + auth_type: 'unknown', + error_details: ensureError(error).message, + }, + }; } if (decoded?.meta?.isOAuth === true) { @@ -53,21 +62,32 @@ export class McpServerMiddlewareService { const authorizationHeader = req.header('authorization'); if (!authorizationHeader) { - this.responseWithUnauthorized(res, req); + this.responseWithUnauthorized(res, req, { + reason: 'missing_authorization_header', + auth_type: 'unknown', + error_details: 'Authorization header not sent', + }); return; } - const token = this.extractBearerToken(authorizationHeader); - - if (!token) { - this.responseWithUnauthorized(res, req); + let token: string; + try { + token = this.extractBearerToken(authorizationHeader); + } catch (er) { + const error = ensureError(er); + this.responseWithUnauthorized(res, req, { + reason: 'invalid_bearer_format', + auth_type: 'unknown', + error_details: error.message, + }); return; } - const user = await this.getUserForToken(token); + const result = await this.getUserForToken(token); + const user = result.user; if (!user) { - this.responseWithUnauthorized(res, req); + this.responseWithUnauthorized(res, req, result.context); return; } @@ -77,9 +97,9 @@ export class McpServerMiddlewareService { }; } - private extractBearerToken(headerValue: string): string | null { + private extractBearerToken(headerValue: string): string { if (!headerValue.startsWith('Bearer')) { - throw new AuthError('Invalid authorization header format'); + throw new AuthError('Invalid authorization header format - Missing Bearer prefix'); } const tokenMatch = headerValue.match(/^Bearer\s+(.+)$/i); @@ -87,23 +107,27 @@ export class McpServerMiddlewareService { return tokenMatch[1]; } - throw new AuthError('Invalid authorization header format'); + throw new AuthError('Invalid authorization header format - Malformed Bearer token'); } - private responseWithUnauthorized(res: Response, req: Request) { - this.trackUnauthorizedEvent(req); + private responseWithUnauthorized(res: Response, req: Request, context?: TelemetryAuthContext) { + this.trackUnauthorizedEvent(req, context); // RFC 6750 Section 3: Include WWW-Authenticate header for 401 responses res.header('WWW-Authenticate', 'Bearer realm="n8n MCP Server"'); - res.status(401).send({ message: UNAUTHORIZED_ERROR_MESSAGE }); + res.status(401).send({ + message: `${UNAUTHORIZED_ERROR_MESSAGE}${context?.error_details ? ': ' + context.error_details : ''}`, + }); } - private trackUnauthorizedEvent(req: Request) { + private trackUnauthorizedEvent(req: Request, context?: TelemetryAuthContext) { const clientInfo = getClientInfo(req); - this.telemetry.track(USER_CONNECTED_TO_MCP_EVENT, { + const payload = { mcp_connection_status: 'error', error: UNAUTHORIZED_ERROR_MESSAGE, client_name: clientInfo?.name, client_version: clientInfo?.version, - }); + ...context, + }; + this.telemetry.track(USER_CONNECTED_TO_MCP_EVENT, payload); } } diff --git a/packages/cli/src/modules/mcp/mcp.errors.ts b/packages/cli/src/modules/mcp/mcp.errors.ts index a937d149cfa..c39039b4c75 100644 --- a/packages/cli/src/modules/mcp/mcp.errors.ts +++ b/packages/cli/src/modules/mcp/mcp.errors.ts @@ -1,6 +1,8 @@ import { Time } from '@n8n/constants'; import { UserError } from 'n8n-workflow'; +import { AuthError } from '@/errors/response-errors/auth.error'; + /** * Error thrown when MCP workflow execution times out */ @@ -17,3 +19,17 @@ export class McpExecutionTimeoutError extends UserError { this.timeoutMs = timeoutMs; } } + +export class JWTVerificationError extends AuthError { + constructor() { + super('JWT Verification Failed'); + this.name = 'JWTVerificationError'; + } +} + +export class AccessTokenNotFoundError extends AuthError { + constructor() { + super('Access Token Not Found in Database'); + this.name = 'AccessTokenNotFoundError'; + } +} diff --git a/packages/cli/src/modules/mcp/mcp.types.ts b/packages/cli/src/modules/mcp/mcp.types.ts index a8987557b80..39586764354 100644 --- a/packages/cli/src/modules/mcp/mcp.types.ts +++ b/packages/cli/src/modules/mcp/mcp.types.ts @@ -1,4 +1,5 @@ import { type ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { User } from '@n8n/db'; import type { INode } from 'n8n-workflow'; import type z from 'zod'; @@ -92,3 +93,26 @@ type SupportedTriggerNodeTypes = keyof typeof SUPPORTED_MCP_TRIGGERS; export type MCPTriggersMap = { [K in SupportedTriggerNodeTypes]: INode[]; }; + +export type AuthFailureReason = + | 'missing_authorization_header' + | 'invalid_bearer_format' + | 'jwt_decode_failed' + | 'invalid_token' + | 'token_not_found_in_db' + | 'user_not_found' + | 'user_id_not_in_auth_info' + | 'unknown_error'; + +export type Mcpauth_type = 'oauth' | 'api_key' | 'unknown'; + +export type TelemetryAuthContext = { + reason: AuthFailureReason; + auth_type: Mcpauth_type; + error_details?: string; +}; + +export type UserWithContext = { + user: User | null; + context?: TelemetryAuthContext; +}; diff --git a/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/MCPConnectionInstructions.vue b/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/MCPConnectionInstructions.vue index ef16ce1df75..57a86d3db6b 100644 --- a/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/MCPConnectionInstructions.vue +++ b/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/MCPConnectionInstructions.vue @@ -8,6 +8,7 @@ import type { OAuthClientResponseDto } from '@n8n/api-types'; import OAuthConnectionInstructions from '@/features/ai/mcpAccess/components/connectionInstructions/OAuthConnectionInstructions.vue'; import AccessTokenConnectionInstructions from '@/features/ai/mcpAccess/components/connectionInstructions/AccessTokenConnectionInstructions.vue'; import { MCP_DOCS_PAGE_URL } from '@/features/ai/mcpAccess/mcp.constants'; +import { useTelemetry } from '@/app/composables/useTelemetry'; const MCP_ENDPOINT = 'mcp-server/http'; @@ -30,6 +31,7 @@ const emit = defineEmits<{ }>(); const i18n = useI18n(); +const telemetry = useTelemetry(); const selectedTab = ref('oauth'); @@ -47,6 +49,23 @@ const tabs = ref>>([ const onTabSelected = (tab: ConnectionTabType) => { selectedTab.value = tab; }; + +const onClientRevoked = (client: OAuthClientResponseDto) => { + emit('revokeClient', client); + telemetry.track('User revoked access for MCP OAuth client', { + client_name: client.name, + }); +}; + +const trackCopyEvent = (payload: { + item: 'server-url' | 'access-token' | 'mcp-json'; + source: 'oauth-tab' | 'token-tab'; +}) => { + telemetry.track('User copied MCP connection parameter', { + parameter: payload.item, + source: payload.source, + }); +};