From 0ee9fbb0b687cc7b949e929f197f6af8ce2f8da2 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Wed, 25 Jun 2025 11:28:34 +0530 Subject: [PATCH] Add auth token based authentication for klavis mcp servers --- apps/rowboat/app/actions/klavis_actions.ts | 75 ++++++-- apps/rowboat/app/lib/constants/klavis.ts | 31 ++++ .../tools/components/AuthTokenModal.tsx | 175 ++++++++++++++++++ .../tools/components/HostedServers.tsx | 158 +++++++++++----- 4 files changed, 374 insertions(+), 65 deletions(-) create mode 100644 apps/rowboat/app/lib/constants/klavis.ts create mode 100644 apps/rowboat/app/projects/[projectId]/tools/components/AuthTokenModal.tsx diff --git a/apps/rowboat/app/actions/klavis_actions.ts b/apps/rowboat/app/actions/klavis_actions.ts index 2036ab53..e910c3cc 100644 --- a/apps/rowboat/app/actions/klavis_actions.ts +++ b/apps/rowboat/app/actions/klavis_actions.ts @@ -9,6 +9,7 @@ import { fetchMcpToolsForServer } from './mcp_actions'; import { headers } from 'next/headers'; import { authorizeUserAction } from './billing_actions'; import { redisClient } from '../lib/redis'; +import { SERVER_URL_PARAMS, SERVER_CLIENT_ID_MAP } from '../lib/constants/klavis'; type McpServerType = z.infer; type McpToolType = z.infer; @@ -674,6 +675,14 @@ export async function enableServer( const instance = instances.find(i => i.name === serverName); if (instance?.id) { + // Check if this server uses auth token (authNeeded but no OAuth) + const usesAuthToken = instance.authNeeded && !SERVER_URL_PARAMS[serverName]; + + if (usesAuthToken) { + // Delete auth data first + await deleteServerAuthData(instance.id); + } + await deleteMcpServerInstance(instance.id, projectId); console.log('[Klavis API] Disabled server:', { serverName, instanceId: instance.id }); @@ -748,26 +757,6 @@ export async function deleteMcpServerInstance( } } -// Server name to URL parameter mapping -const SERVER_URL_PARAMS: Record = { - 'Google Calendar': 'gcalendar', - 'Google Drive': 'gdrive', - 'Google Docs': 'gdocs', - 'Google Sheets': 'gsheets', - 'Gmail': 'gmail', -}; - -// Server name to environment variable mapping for client IDs -const SERVER_CLIENT_ID_MAP: Record = { - 'GitHub': process.env.KLAVIS_GITHUB_CLIENT_ID, - 'Google Calendar': process.env.KLAVIS_GOOGLE_CLIENT_ID, - 'Google Drive': process.env.KLAVIS_GOOGLE_CLIENT_ID, - 'Google Docs': process.env.KLAVIS_GOOGLE_CLIENT_ID, - 'Google Sheets': process.env.KLAVIS_GOOGLE_CLIENT_ID, - 'Gmail': process.env.KLAVIS_GOOGLE_CLIENT_ID, - 'Slack': process.env.KLAVIS_SLACK_ID, -}; - export async function generateServerAuthUrl( serverName: string, projectId: string, @@ -868,4 +857,50 @@ export async function syncServerTools(projectId: string, serverName: string): Pr }); throw error; } +} + +// Auth Token Management Functions +export async function setServerAuthToken( + instanceId: string, + authToken: string +): Promise<{ success: boolean; message?: string; error?: string }> { + try { + const response = await klavisApiCall<{ success: boolean; message: string }>( + `/mcp-server/instance/set-auth-token`, + { + method: 'POST', + body: { instanceId, authToken } + } + ); + + return { success: true, message: response.message }; + } catch (error: any) { + // Handle 422 validation errors + if (error.message.includes('422')) { + try { + const errorData = JSON.parse(error.message); + const validationErrors = errorData.detail?.map((err: any) => err.msg).join(', '); + return { success: false, error: validationErrors || 'Invalid auth token' }; + } catch { + return { success: false, error: 'Invalid auth token format' }; + } + } + + // Handle other errors + return { success: false, error: 'Failed to set auth token. Please try again.' }; + } +} + +export async function deleteServerAuthData(instanceId: string): Promise { + try { + await klavisApiCall<{ success: boolean; message: string }>( + `/mcp-server/instance/delete-auth/${instanceId}`, + { method: 'DELETE' } + ); + console.log('[Klavis API] Auth data deleted for instance:', instanceId); + } catch (error: any) { + // Log error but don't fail the deletion process + console.error('[Klavis API] Failed to delete auth data:', error); + // Don't throw - auth cleanup failure shouldn't prevent server deletion + } } \ No newline at end of file diff --git a/apps/rowboat/app/lib/constants/klavis.ts b/apps/rowboat/app/lib/constants/klavis.ts new file mode 100644 index 00000000..14a912f3 --- /dev/null +++ b/apps/rowboat/app/lib/constants/klavis.ts @@ -0,0 +1,31 @@ +// Server name to URL parameter mapping +export const SERVER_URL_PARAMS: Record = { + 'Google Calendar': 'gcalendar', + 'Google Drive': 'gdrive', + 'Google Docs': 'gdocs', + 'Google Sheets': 'gsheets', + 'Gmail': 'gmail', + 'GitHub': 'github', + 'Slack': 'slack', + 'Jira': 'jira', + 'Notion': 'notion', + 'Supabase': 'supabase', + 'WordPress': 'wordpress', + 'Asana': 'asana', + 'Close': 'close', + 'Confluence': 'confluence', + 'Salesforce': 'salesforce', + 'Linear': 'linear', + 'Attio': 'attio' +}; + +// Server name to environment variable mapping for client IDs +export const SERVER_CLIENT_ID_MAP: Record = { + 'GitHub': process.env.KLAVIS_GITHUB_CLIENT_ID, + 'Google Calendar': process.env.KLAVIS_GOOGLE_CLIENT_ID, + 'Google Drive': process.env.KLAVIS_GOOGLE_CLIENT_ID, + 'Google Docs': process.env.KLAVIS_GOOGLE_CLIENT_ID, + 'Google Sheets': process.env.KLAVIS_GOOGLE_CLIENT_ID, + 'Gmail': process.env.KLAVIS_GOOGLE_CLIENT_ID, + 'Slack': process.env.KLAVIS_SLACK_ID, +}; \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/AuthTokenModal.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/AuthTokenModal.tsx new file mode 100644 index 00000000..b91189a4 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/tools/components/AuthTokenModal.tsx @@ -0,0 +1,175 @@ +'use client'; + +import { useState } from 'react'; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner } from "@heroui/react"; +import { Button } from "@/components/ui/button"; +import { Key, AlertCircle, Eye, EyeOff } from "lucide-react"; +import { setServerAuthToken } from '@/app/actions/klavis_actions'; +import { MCPServer } from '@/app/lib/types/types'; +import { z } from 'zod'; + +type McpServerType = z.infer; + +interface AuthTokenModalProps { + isOpen: boolean; + onClose: () => void; + server: McpServerType | null; + onSuccess: () => void; +} + +export function AuthTokenModal({ isOpen, onClose, server, onSuccess }: AuthTokenModalProps) { + const [authToken, setAuthToken] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [showToken, setShowToken] = useState(false); + + const handleSubmit = async () => { + if (!server?.instanceId || !authToken.trim()) { + setError('Please enter a valid auth token'); + return; + } + + setIsSubmitting(true); + setError(null); + + try { + const result = await setServerAuthToken(server.instanceId, authToken.trim()); + + if (result.success) { + // Success - close modal and refresh data + setAuthToken(''); + setError(null); + onSuccess(); + onClose(); + } else { + // Show validation error + setError(result.error || 'Failed to set auth token'); + } + } catch (err) { + setError('Network error. Please check your connection and try again.'); + } finally { + setIsSubmitting(false); + } + }; + + const handleClose = () => { + setAuthToken(''); + setError(null); + onClose(); + }; + + if (!server) return null; + + return ( + + + + + Authenticate {server.name} + + +
+
+

+ You'll need to obtain an authentication token from {server.name}. Please refer to their documentation or settings page to find your API key or access token. +

+
+ +
+ +
+ setAuthToken(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !isSubmitting) { + handleSubmit(); + } + }} + className="w-full pr-10 pl-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-base text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-900 border-0 shadow-none" + disabled={isSubmitting} + autoComplete="off" + /> + +
+
+ + {error && ( +
+ +

{error}

+
+ )} +
+ +
+ + + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/HostedServers.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/HostedServers.tsx index 175c7b69..9e0dc2c5 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/HostedServers.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/HostedServers.tsx @@ -21,6 +21,8 @@ import { ToolManagementPanel, } from './MCPServersCommon'; import { BillingUpgradeModal } from '@/components/common/billing-upgrade-modal'; +import { AuthTokenModal } from './AuthTokenModal'; +import { SERVER_URL_PARAMS } from '@/app/lib/constants/klavis'; type McpServerType = z.infer; type McpToolType = z.infer['tools'][number]; @@ -140,6 +142,8 @@ export function HostedServers({ onSwitchTab }: HostedServersProps) { const [serverToolCounts, setServerToolCounts] = useState>(new Map()); const [syncingServers, setSyncingServers] = useState>(new Set()); const [billingError, setBillingError] = useState(null); + const [showAuthTokenModal, setShowAuthTokenModal] = useState(false); + const [selectedServerForAuth, setSelectedServerForAuth] = useState(null); const fetchServers = useCallback(async () => { try { @@ -362,63 +366,74 @@ export function HostedServers({ onSwitchTab }: HostedServersProps) { if (!server.instanceId) { throw new Error('Server instance ID not found'); } - const authUrl = await generateServerAuthUrl(server.name, projectId, server.instanceId); - const authWindow = window.open( - authUrl, - '_blank', - 'width=600,height=700' - ); - if (authWindow) { - const checkInterval = setInterval(async () => { - if (authWindow.closed) { - clearInterval(checkInterval); - - try { - setServerOperations(prev => { - const next = new Map(prev); - next.set(server.name, 'checking-auth'); - return next; - }); + // Check if this server uses OAuth (in SERVER_URL_PARAMS) or auth token + const usesOAuth = SERVER_URL_PARAMS[server.name]; + + if (usesOAuth) { + // Use existing OAuth flow + const authUrl = await generateServerAuthUrl(server.name, projectId, server.instanceId); + const authWindow = window.open( + authUrl, + '_blank', + 'width=600,height=700' + ); + + if (authWindow) { + const checkInterval = setInterval(async () => { + if (authWindow.closed) { + clearInterval(checkInterval); - await updateProjectServers(projectId, server.name); - - const response = await listAvailableMcpServers(projectId); - if (response.data) { - const updatedServer = response.data.find(us => us.name === server.name); - if (updatedServer) { - setServers(prevServers => { - return prevServers.map(s => { - if (s.name === server.name) { - return { ...updatedServer, serverType: 'hosted' as const }; - } - return s; + try { + setServerOperations(prev => { + const next = new Map(prev); + next.set(server.name, 'checking-auth'); + return next; + }); + + await updateProjectServers(projectId, server.name); + + const response = await listAvailableMcpServers(projectId); + if (response.data) { + const updatedServer = response.data.find(us => us.name === server.name); + if (updatedServer) { + setServers(prevServers => { + return prevServers.map(s => { + if (s.name === server.name) { + return { ...updatedServer, serverType: 'hosted' as const }; + } + return s; + }); }); - }); - if (selectedServer?.name === server.name) { - setSelectedServer({ ...updatedServer, serverType: 'hosted' as const }); - } + if (selectedServer?.name === server.name) { + setSelectedServer({ ...updatedServer, serverType: 'hosted' as const }); + } - if (!server.authNeeded || updatedServer.isAuthenticated) { - await handleSyncServer(updatedServer); + if (!server.authNeeded || updatedServer.isAuthenticated) { + await handleSyncServer(updatedServer); + } } } + } finally { + setServerOperations(prev => { + const next = new Map(prev); + next.delete(server.name); + return next; + }); } - } finally { - setServerOperations(prev => { - const next = new Map(prev); - next.delete(server.name); - return next; - }); } - } - }, 500); + }, 500); + } else { + window.alert('Failed to open authentication window. Please check your popup blocker settings.'); + } } else { - window.alert('Failed to open authentication window. Please check your popup blocker settings.'); + // Use auth token modal + setSelectedServerForAuth(server); + setShowAuthTokenModal(true); } } catch (error) { - console.error('[Auth] Error initiating OAuth:', error); + console.error('[Auth] Error initiating authentication:', error); window.alert('Failed to setup authentication'); } }; @@ -515,6 +530,49 @@ export function HostedServers({ onSwitchTab }: HostedServersProps) { } }; + const handleAuthTokenSuccess = async () => { + if (!selectedServerForAuth) return; + + try { + setServerOperations(prev => { + const next = new Map(prev); + next.set(selectedServerForAuth.name, 'checking-auth'); + return next; + }); + + await updateProjectServers(projectId, selectedServerForAuth.name); + + const response = await listAvailableMcpServers(projectId); + if (response.data) { + const updatedServer = response.data.find(us => us.name === selectedServerForAuth.name); + if (updatedServer) { + setServers(prevServers => { + return prevServers.map(s => { + if (s.name === selectedServerForAuth.name) { + return { ...updatedServer, serverType: 'hosted' as const }; + } + return s; + }); + }); + + if (selectedServer?.name === selectedServerForAuth.name) { + setSelectedServer({ ...updatedServer, serverType: 'hosted' as const }); + } + + if (!selectedServerForAuth.authNeeded || updatedServer.isAuthenticated) { + await handleSyncServer(updatedServer); + } + } + } + } finally { + setServerOperations(prev => { + const next = new Map(prev); + next.delete(selectedServerForAuth.name); + return next; + }); + } + }; + const filteredServers = sortServers(servers.filter(server => { const searchLower = searchQuery.toLowerCase(); const serverTools = server.tools || []; @@ -713,6 +771,16 @@ export function HostedServers({ onSwitchTab }: HostedServersProps) { onClose={() => setBillingError(null)} errorMessage={billingError || ''} /> + + { + setShowAuthTokenModal(false); + setSelectedServerForAuth(null); + }} + server={selectedServerForAuth} + onSuccess={handleAuthTokenSuccess} + /> ); } \ No newline at end of file