From 2e1fc2e8f995197404d1286ea7d5a2546b10e01f Mon Sep 17 00:00:00 2001 From: Felarof Date: Fri, 20 Feb 2026 06:01:59 -0800 Subject: [PATCH] feat: add API key auth flow for Klavis MCP servers (#343) * feat: update to support more klavis MCP servers * fix: minor icon fix * fix: normalize klavis mcp auth flow compatibility * feat: add API key auth flow for Klavis MCP servers Servers that use API key authentication (Stripe, Cloudflare, Brave Search, Exa, Mem0, Resend, Mixpanel, PostHog, Postman, Zendesk, Intercom) were failing with "Failed to add app" because the frontend only handled OAuth flows. This adds the complete API key auth path: - Backend: apiKeyUrls in StrataCreateResponse, submitApiKey() method, /servers/submit-api-key route - Frontend: ApiKeyDialog component, useSubmitApiKey hook, ConnectMCP updated to show dialog for API-key servers instead of opening OAuth Co-Authored-By: Claude Opus 4.6 * fix: remove broken success check in Klavis submitApiKey The Klavis /mcp-server/instance/set-auth endpoint returns { message: "Authentication updated successfully." } without a success field. Our code checked `data.success` which was always undefined, causing API key auth to fail even when Klavis accepted the key. The request() method already throws on non-2xx responses, so the explicit check was redundant and incorrect. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- apps/agent/assets/mcp-icons/asana.svg | 6 + apps/agent/assets/mcp-icons/box.svg | 10 + apps/agent/assets/mcp-icons/brave_search.svg | 1 + apps/agent/assets/mcp-icons/cal_com.svg | 252 ++++++++++++++++++ apps/agent/assets/mcp-icons/clickup.svg | 1 + apps/agent/assets/mcp-icons/cloudflare.svg | 1 + apps/agent/assets/mcp-icons/discord.svg | 12 + apps/agent/assets/mcp-icons/dropbox.svg | 1 + apps/agent/assets/mcp-icons/exa.png | Bin 0 -> 3176 bytes apps/agent/assets/mcp-icons/google_forms.svg | 1 + apps/agent/assets/mcp-icons/hubspot.svg | 20 ++ apps/agent/assets/mcp-icons/intercom.svg | 4 + apps/agent/assets/mcp-icons/memo.webp | Bin 0 -> 1502 bytes .../assets/mcp-icons/microsoft_teams.svg | 22 ++ apps/agent/assets/mcp-icons/mixpanel.svg | 23 ++ apps/agent/assets/mcp-icons/monday.svg | 7 + apps/agent/assets/mcp-icons/onedrive.svg | 1 + apps/agent/assets/mcp-icons/outlook.svg | 1 + apps/agent/assets/mcp-icons/postman.svg | 1 + apps/agent/assets/mcp-icons/resend.svg | 3 + apps/agent/assets/mcp-icons/shopify.svg | 1 + apps/agent/assets/mcp-icons/stripe.svg | 5 + apps/agent/assets/mcp-icons/supabase.svg | 15 ++ apps/agent/assets/mcp-icons/vercel.svg | 3 + apps/agent/assets/mcp-icons/whatsapp.webp | Bin 0 -> 3110 bytes apps/agent/assets/mcp-icons/wordpress.svg | 1 + apps/agent/assets/mcp-icons/youtube.svg | 25 ++ apps/agent/assets/mcp-icons/zendesk.svg | 1 + .../app/connect-mcp/ApiKeyDialog.tsx | 2 +- .../app/connect-mcp/McpServerIcon.tsx | 57 ++++ apps/server/src/agent/prompt.ts | 10 +- apps/server/src/api/routes/klavis.ts | 38 ++- .../src/lib/clients/klavis/klavis-client.ts | 62 +++-- .../lib/clients/klavis/oauth-mcp-servers.ts | 29 ++ apps/server/tests/api/routes/klavis.test.ts | 92 +++++++ 35 files changed, 686 insertions(+), 22 deletions(-) create mode 100644 apps/agent/assets/mcp-icons/asana.svg create mode 100644 apps/agent/assets/mcp-icons/box.svg create mode 100644 apps/agent/assets/mcp-icons/brave_search.svg create mode 100644 apps/agent/assets/mcp-icons/cal_com.svg create mode 100644 apps/agent/assets/mcp-icons/clickup.svg create mode 100644 apps/agent/assets/mcp-icons/cloudflare.svg create mode 100644 apps/agent/assets/mcp-icons/discord.svg create mode 100644 apps/agent/assets/mcp-icons/dropbox.svg create mode 100644 apps/agent/assets/mcp-icons/exa.png create mode 100644 apps/agent/assets/mcp-icons/google_forms.svg create mode 100644 apps/agent/assets/mcp-icons/hubspot.svg create mode 100644 apps/agent/assets/mcp-icons/intercom.svg create mode 100644 apps/agent/assets/mcp-icons/memo.webp create mode 100644 apps/agent/assets/mcp-icons/microsoft_teams.svg create mode 100644 apps/agent/assets/mcp-icons/mixpanel.svg create mode 100644 apps/agent/assets/mcp-icons/monday.svg create mode 100644 apps/agent/assets/mcp-icons/onedrive.svg create mode 100644 apps/agent/assets/mcp-icons/outlook.svg create mode 100644 apps/agent/assets/mcp-icons/postman.svg create mode 100644 apps/agent/assets/mcp-icons/resend.svg create mode 100644 apps/agent/assets/mcp-icons/shopify.svg create mode 100644 apps/agent/assets/mcp-icons/stripe.svg create mode 100644 apps/agent/assets/mcp-icons/supabase.svg create mode 100644 apps/agent/assets/mcp-icons/vercel.svg create mode 100644 apps/agent/assets/mcp-icons/whatsapp.webp create mode 100644 apps/agent/assets/mcp-icons/wordpress.svg create mode 100644 apps/agent/assets/mcp-icons/youtube.svg create mode 100644 apps/agent/assets/mcp-icons/zendesk.svg create mode 100644 apps/server/tests/api/routes/klavis.test.ts diff --git a/apps/agent/assets/mcp-icons/asana.svg b/apps/agent/assets/mcp-icons/asana.svg new file mode 100644 index 000000000..8a89c47f4 --- /dev/null +++ b/apps/agent/assets/mcp-icons/asana.svg @@ -0,0 +1,6 @@ + + + + diff --git a/apps/agent/assets/mcp-icons/box.svg b/apps/agent/assets/mcp-icons/box.svg new file mode 100644 index 000000000..1c88da6b6 --- /dev/null +++ b/apps/agent/assets/mcp-icons/box.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/agent/assets/mcp-icons/brave_search.svg b/apps/agent/assets/mcp-icons/brave_search.svg new file mode 100644 index 000000000..062dc8940 --- /dev/null +++ b/apps/agent/assets/mcp-icons/brave_search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/cal_com.svg b/apps/agent/assets/mcp-icons/cal_com.svg new file mode 100644 index 000000000..7d6972a22 --- /dev/null +++ b/apps/agent/assets/mcp-icons/cal_com.svg @@ -0,0 +1,252 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/clickup.svg b/apps/agent/assets/mcp-icons/clickup.svg new file mode 100644 index 000000000..0ae45e9be --- /dev/null +++ b/apps/agent/assets/mcp-icons/clickup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/cloudflare.svg b/apps/agent/assets/mcp-icons/cloudflare.svg new file mode 100644 index 000000000..ebfa9f0f2 --- /dev/null +++ b/apps/agent/assets/mcp-icons/cloudflare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/discord.svg b/apps/agent/assets/mcp-icons/discord.svg new file mode 100644 index 000000000..5806fa7fc --- /dev/null +++ b/apps/agent/assets/mcp-icons/discord.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/dropbox.svg b/apps/agent/assets/mcp-icons/dropbox.svg new file mode 100644 index 000000000..e467522a5 --- /dev/null +++ b/apps/agent/assets/mcp-icons/dropbox.svg @@ -0,0 +1 @@ + diff --git a/apps/agent/assets/mcp-icons/exa.png b/apps/agent/assets/mcp-icons/exa.png new file mode 100644 index 0000000000000000000000000000000000000000..09a05730d27ba67ec2dbcfcf72f2666ce1360687 GIT binary patch literal 3176 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 literal 0 HcmV?d00001 diff --git a/apps/agent/assets/mcp-icons/google_forms.svg b/apps/agent/assets/mcp-icons/google_forms.svg new file mode 100644 index 000000000..aa541f98a --- /dev/null +++ b/apps/agent/assets/mcp-icons/google_forms.svg @@ -0,0 +1 @@ + diff --git a/apps/agent/assets/mcp-icons/hubspot.svg b/apps/agent/assets/mcp-icons/hubspot.svg new file mode 100644 index 000000000..be31d302d --- /dev/null +++ b/apps/agent/assets/mcp-icons/hubspot.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/agent/assets/mcp-icons/intercom.svg b/apps/agent/assets/mcp-icons/intercom.svg new file mode 100644 index 000000000..157dd7559 --- /dev/null +++ b/apps/agent/assets/mcp-icons/intercom.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/memo.webp b/apps/agent/assets/mcp-icons/memo.webp new file mode 100644 index 0000000000000000000000000000000000000000..ed5fa18b9b30aac921b98f6cbe21e63ce4ef09df GIT binary patch literal 1502 zcmV<41tI!UNk&H21pok7MM6+kP&gpU1pok$8UUREDqsL$06vjIm`Nq1A|Wcf`KYiG z31a}ttbiI&hbtb3`|E!B{6qRv+fT%vdl3=q5B3}&?-l-0{!@Ac0DedP7rUoXbwMZr zXE}j16jnTf{ zSt5|wD%DE=F-e~fHs?_RNIsE1TE}?vpczP7WsoW0NYdZ5(n7;5bdjIfFASf?@#062TuUdcYt@ux(4{+E(0RH$LQnUV|ZVreh zMr(R=CYmP~Nu+u$&d2E3cko+8^L=hoiBRHjJO9+zt%vcG&Y%j44sFmAzbm?*=N}kH z#27xjWYXrx2R1T`is-^bvj6GN_*OQs1eq*4T+-=gpsYSJhxlq~EZM@1?lAWTC%QAI z2`~=_|1zE!FrdqnM*|=f08H3GFGNMoFU*XJiQMJh9?!Cc6nyYt@k`I)`A?^N?3ieIEZSm!DlmZ6Z~#x1l?2Vr`TBw;Zf!CnDeVipP%Dv z>f=x+F#j9Bz5wt{OH~+0X0%598#JkK_<-A3HzIyK>KzcVy-OJ(2Uvj(EZ5ho0y&F# zI%TwrnCSg6uLem>lz1%Wm$r=v%*Y01b~iut(@!R%#Tn_o;YP zrsC07vS{BjQJ=HAXa5s`2K^WbUE|ix-`Di#@E3?D{k~D%tNL2ziY*F_94F)X_ihhP z6K3B1du-;3<;Pm<62srxo$HG6T)%q2Gj~AT0aezbH{%RINJheL+T65BJ^zJ(c0dBlRUJ0Q7kTWS2aP7o*iZ z_U{dWQ4JjBz0708hFxa9a@(y6Citlrjq>{LoGURNNs-gF36;F3gGpE1Wlo~vPk}M% zb}L3Mtj>)T!C6f@AS|<}8tlx{oxX0xK*w3<&KOB##iM0OL%48d&UX;Sg{rxt+|xvt zY^l1xQDsp@3{Yw@WhZ7?WjLOv^>p^5fFQ8(bSQh)ogo8JW@h>X&7bj#o{_Y7k`zH= zG#oNTp2HnS8|`Qc^Jj|s@ikvZDC8Oc%Okm)UK8{Ud!;Qylf%-I;q>mX;vKpzviFOx zcCq%6=u(^sECM%jxI7*O$_@TF>^r85sV0mapZn)Zyfjl*A?(Ot_RaWc8Ls8U>ZsLoP((FTxZA8eRHo%(yVw7 zB3*{$I*AE_9G#nvQ@Iyn#m4(HxUb1vEI2|(A)Oxw=-(SIecJsNf&a?i;LCY7#W`h% zH@qUTgquQwBsR9V06V4p>vBXj``!ZG#m>&l-pV+wa>)XEabOkS%hJI$Di4s%(ht#j zA`SdJ_vnD*3d5(S)AQRz`b=_zx`4@jt}wDqnsW2)m|qVHX*w+c=;fuoe5F4!2R&c_ E0J|aaB>(^b literal 0 HcmV?d00001 diff --git a/apps/agent/assets/mcp-icons/microsoft_teams.svg b/apps/agent/assets/mcp-icons/microsoft_teams.svg new file mode 100644 index 000000000..3409e6cfa --- /dev/null +++ b/apps/agent/assets/mcp-icons/microsoft_teams.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/agent/assets/mcp-icons/mixpanel.svg b/apps/agent/assets/mcp-icons/mixpanel.svg new file mode 100644 index 000000000..0a983dfcf --- /dev/null +++ b/apps/agent/assets/mcp-icons/mixpanel.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/monday.svg b/apps/agent/assets/mcp-icons/monday.svg new file mode 100644 index 000000000..795d7c7fc --- /dev/null +++ b/apps/agent/assets/mcp-icons/monday.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/onedrive.svg b/apps/agent/assets/mcp-icons/onedrive.svg new file mode 100644 index 000000000..f7d7a6a60 --- /dev/null +++ b/apps/agent/assets/mcp-icons/onedrive.svg @@ -0,0 +1 @@ +OfficeCore10_32x_24x_20x_16x_01-22-2019 \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/outlook.svg b/apps/agent/assets/mcp-icons/outlook.svg new file mode 100644 index 000000000..4e1be88f9 --- /dev/null +++ b/apps/agent/assets/mcp-icons/outlook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/postman.svg b/apps/agent/assets/mcp-icons/postman.svg new file mode 100644 index 000000000..9d077b1b1 --- /dev/null +++ b/apps/agent/assets/mcp-icons/postman.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/resend.svg b/apps/agent/assets/mcp-icons/resend.svg new file mode 100644 index 000000000..506dad83c --- /dev/null +++ b/apps/agent/assets/mcp-icons/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/agent/assets/mcp-icons/shopify.svg b/apps/agent/assets/mcp-icons/shopify.svg new file mode 100644 index 000000000..084e09437 --- /dev/null +++ b/apps/agent/assets/mcp-icons/shopify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/stripe.svg b/apps/agent/assets/mcp-icons/stripe.svg new file mode 100644 index 000000000..df61834df --- /dev/null +++ b/apps/agent/assets/mcp-icons/stripe.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/agent/assets/mcp-icons/supabase.svg b/apps/agent/assets/mcp-icons/supabase.svg new file mode 100644 index 000000000..cf8e0f29a --- /dev/null +++ b/apps/agent/assets/mcp-icons/supabase.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/agent/assets/mcp-icons/vercel.svg b/apps/agent/assets/mcp-icons/vercel.svg new file mode 100644 index 000000000..72948d01a --- /dev/null +++ b/apps/agent/assets/mcp-icons/vercel.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/agent/assets/mcp-icons/whatsapp.webp b/apps/agent/assets/mcp-icons/whatsapp.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b2b4476255b59b408000cad05032a8b7ae9e448 GIT binary patch literal 3110 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 A!2kdN literal 0 HcmV?d00001 diff --git a/apps/agent/assets/mcp-icons/wordpress.svg b/apps/agent/assets/mcp-icons/wordpress.svg new file mode 100644 index 000000000..72183e904 --- /dev/null +++ b/apps/agent/assets/mcp-icons/wordpress.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/youtube.svg b/apps/agent/assets/mcp-icons/youtube.svg new file mode 100644 index 000000000..50b99cd3d --- /dev/null +++ b/apps/agent/assets/mcp-icons/youtube.svg @@ -0,0 +1,25 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/agent/assets/mcp-icons/zendesk.svg b/apps/agent/assets/mcp-icons/zendesk.svg new file mode 100644 index 000000000..1d1de4111 --- /dev/null +++ b/apps/agent/assets/mcp-icons/zendesk.svg @@ -0,0 +1 @@ + diff --git a/apps/agent/entrypoints/app/connect-mcp/ApiKeyDialog.tsx b/apps/agent/entrypoints/app/connect-mcp/ApiKeyDialog.tsx index ad4dceb60..9c1057c18 100644 --- a/apps/agent/entrypoints/app/connect-mcp/ApiKeyDialog.tsx +++ b/apps/agent/entrypoints/app/connect-mcp/ApiKeyDialog.tsx @@ -51,7 +51,7 @@ export const ApiKeyDialog: FC = ({ }, }) - // biome-ignore lint/correctness/useExhaustiveDependencies: We only want to reset the form when the dialog is closed, not on every render + // biome-ignore lint/correctness/useExhaustiveDependencies: reset form when dialog closes useEffect(() => { if (!open) form.reset() }, [open]) diff --git a/apps/agent/entrypoints/app/connect-mcp/McpServerIcon.tsx b/apps/agent/entrypoints/app/connect-mcp/McpServerIcon.tsx index ab8f3701d..f846e8d5c 100644 --- a/apps/agent/entrypoints/app/connect-mcp/McpServerIcon.tsx +++ b/apps/agent/entrypoints/app/connect-mcp/McpServerIcon.tsx @@ -1,8 +1,17 @@ import { Server } from 'lucide-react' import type { FC } from 'react' import AirtableSvg from '@/assets/mcp-icons/airtable.svg' +import AsanaSvg from '@/assets/mcp-icons/asana.svg' +import BoxSvg from '@/assets/mcp-icons/box.svg' +import BraveSearchSvg from '@/assets/mcp-icons/brave_search.svg' +import CalComSvg from '@/assets/mcp-icons/cal_com.svg' import CanvaSvg from '@/assets/mcp-icons/canva.svg' +import ClickUpSvg from '@/assets/mcp-icons/clickup.svg' +import CloudflareSvg from '@/assets/mcp-icons/cloudflare.svg' import ConfluenceSvg from '@/assets/mcp-icons/confluence.svg' +import DiscordSvg from '@/assets/mcp-icons/discord.svg' +import DropboxSvg from '@/assets/mcp-icons/dropbox.svg' +import ExaPng from '@/assets/mcp-icons/exa.png' import FigmaSvg from '@/assets/mcp-icons/figma.svg' import GithubSvg from '@/assets/mcp-icons/github.svg' import GitlabSvg from '@/assets/mcp-icons/gitlab.svg' @@ -11,13 +20,32 @@ import GoogleSvg from '@/assets/mcp-icons/google.svg' import GoogleCalendarSvg from '@/assets/mcp-icons/google_calendar.svg' import GoogleDocsSvg from '@/assets/mcp-icons/google_docs_editors.svg' import GoogleDriveSvg from '@/assets/mcp-icons/google_drive.svg' +import GoogleFormsSvg from '@/assets/mcp-icons/google_forms.svg' +import HubSpotSvg from '@/assets/mcp-icons/hubspot.svg' +import IntercomSvg from '@/assets/mcp-icons/intercom.svg' import JiraSvg from '@/assets/mcp-icons/jira.svg' import LinearSvg from '@/assets/mcp-icons/linear.svg' import LinkedinSvg from '@/assets/mcp-icons/linkedin.svg' +import Mem0Webp from '@/assets/mcp-icons/memo.webp' +import MicrosoftTeamsSvg from '@/assets/mcp-icons/microsoft_teams.svg' +import MixpanelSvg from '@/assets/mcp-icons/mixpanel.svg' +import MondaySvg from '@/assets/mcp-icons/monday.svg' import NotionSvg from '@/assets/mcp-icons/notion.svg' +import OneDriveSvg from '@/assets/mcp-icons/onedrive.svg' +import OutlookSvg from '@/assets/mcp-icons/outlook.svg' import PostHogSvg from '@/assets/mcp-icons/posthog.svg' +import PostmanSvg from '@/assets/mcp-icons/postman.svg' +import ResendSvg from '@/assets/mcp-icons/resend.svg' import SalesforceSvg from '@/assets/mcp-icons/salesforce.svg' +import ShopifySvg from '@/assets/mcp-icons/shopify.svg' import SlackSvg from '@/assets/mcp-icons/slack.svg' +import StripeSvg from '@/assets/mcp-icons/stripe.svg' +import SupabaseSvg from '@/assets/mcp-icons/supabase.svg' +import VercelSvg from '@/assets/mcp-icons/vercel.svg' +import WhatsAppWebp from '@/assets/mcp-icons/whatsapp.webp' +import WordPressSvg from '@/assets/mcp-icons/wordpress.svg' +import YouTubeSvg from '@/assets/mcp-icons/youtube.svg' +import ZendeskSvg from '@/assets/mcp-icons/zendesk.svg' const mcpIconMap: Record = { Gmail: GmailSvg, @@ -25,6 +53,7 @@ const mcpIconMap: Record = { 'Google Docs': GoogleDocsSvg, 'Google Drive': GoogleDriveSvg, 'Google Sheets': GoogleSvg, + 'Google Forms': GoogleFormsSvg, Slack: SlackSvg, LinkedIn: LinkedinSvg, Notion: NotionSvg, @@ -37,7 +66,35 @@ const mcpIconMap: Record = { Figma: FigmaSvg, Canva: CanvaSvg, Salesforce: SalesforceSvg, + ClickUp: ClickUpSvg, + Asana: AsanaSvg, + Monday: MondaySvg, + 'Microsoft Teams': MicrosoftTeamsSvg, + 'Outlook Mail': OutlookSvg, + 'Outlook Calendar': OutlookSvg, + Supabase: SupabaseSvg, + Vercel: VercelSvg, + Postman: PostmanSvg, + Stripe: StripeSvg, + Cloudflare: CloudflareSvg, + 'Brave Search': BraveSearchSvg, + Mem0: Mem0Webp, + Exa: ExaPng, + Dropbox: DropboxSvg, + OneDrive: OneDriveSvg, + WordPress: WordPressSvg, + YouTube: YouTubeSvg, + Box: BoxSvg, + HubSpot: HubSpotSvg, PostHog: PostHogSvg, + Mixpanel: MixpanelSvg, + Discord: DiscordSvg, + WhatsApp: WhatsAppWebp, + Shopify: ShopifySvg, + 'Cal.com': CalComSvg, + Resend: ResendSvg, + Zendesk: ZendeskSvg, + Intercom: IntercomSvg, } interface McpServerIconProps { diff --git a/apps/server/src/agent/prompt.ts b/apps/server/src/agent/prompt.ts index 98f16dc45..09b2a3f96 100644 --- a/apps/server/src/agent/prompt.ts +++ b/apps/server/src/agent/prompt.ts @@ -3,6 +3,9 @@ * Copyright 2025 BrowserOS * SPDX-License-Identifier: AGPL-3.0-or-later */ + +import { OAUTH_MCP_SERVERS } from '../lib/clients/klavis/oauth-mcp-servers' + /** * BrowserOS Agent System Prompt v5 * @@ -251,10 +254,13 @@ Use \`browser_get_bookmarks\` to find existing folder IDs, or create new folders // ----------------------------------------------------------------------------- function getExternalIntegrations(): string { + const serverNames = OAUTH_MCP_SERVERS.map((s) => s.name).join(', ') + const serverCount = OAUTH_MCP_SERVERS.length + return ` ## External Integrations (Klavis Strata) -You have access to 15+ external services, including Gmail, Slack, Google Calendar, Notion, GitHub, and Jira, via Strata tools. Use progressive discovery. +You have access to ${serverCount}+ external services (Gmail, Slack, Google Calendar, Notion, GitHub, Jira, etc.) via Strata tools. Use progressive discovery. 1. \`discover_server_categories_or_actions(user_query, server_names[])\` - **Start here**. Returns categories or actions for specified servers. @@ -281,7 +287,7 @@ When \`execute_action\` fails with an authentication error: ## Available Servers -Gmail, Google Calendar, Google Docs, Google Sheets, Google Drive, Slack, LinkedIn, Notion, Airtable, Confluence, GitHub, GitLab, Linear, Jira, Figma, Canva, Salesforce. +${serverNames}. ## Usage Guidelines - Always discover before executing, do not guess action names diff --git a/apps/server/src/api/routes/klavis.ts b/apps/server/src/api/routes/klavis.ts index 89a74d8ca..2c43de610 100644 --- a/apps/server/src/api/routes/klavis.ts +++ b/apps/server/src/api/routes/klavis.ts @@ -19,6 +19,29 @@ interface KlavisRouteDeps { browserosId: string } +const normalizeServerKey = (value: string): string => + value.toLowerCase().replace(/[^a-z0-9]+/g, '') + +const getAuthUrlForServer = ( + authUrlMap: Record | undefined, + serverName: string, +): string | undefined => { + if (!authUrlMap) { + return undefined + } + const directMatch = authUrlMap[serverName] + if (directMatch) { + return directMatch + } + const targetKey = normalizeServerKey(serverName) + for (const [key, value] of Object.entries(authUrlMap)) { + if (normalizeServerKey(key) === targetKey) { + return value + } + } + return undefined +} + export function createKlavisRoutes(deps: KlavisRouteDeps) { const { browserosId } = deps const klavisClient = new KlavisClient() @@ -67,11 +90,18 @@ export function createKlavisRoutes(deps: KlavisRouteDeps) { try { const integrations = await klavisClient.getUserIntegrations(browserosId) + const normalizedIntegrations = integrations.map((integration) => ({ + name: integration.name, + is_authenticated: integration.isAuthenticated, + })) logger.info('Fetched user integrations', { browserosId: browserosId.slice(0, 12), - count: integrations.length, + count: normalizedIntegrations.length, + }) + return c.json({ + integrations: normalizedIntegrations, + count: normalizedIntegrations.length, }) - return c.json({ integrations, count: integrations.length }) } catch (error) { logger.error('Error fetching user integrations', { browserosId: browserosId?.slice(0, 12), @@ -101,8 +131,8 @@ export function createKlavisRoutes(deps: KlavisRouteDeps) { serverName, strataId: result.strataId, addedServers: result.addedServers, - oauthUrl: result.oauthUrls?.[serverName], - apiKeyUrl: result.apiKeyUrls?.[serverName], + oauthUrl: getAuthUrlForServer(result.oauthUrls, serverName), + apiKeyUrl: getAuthUrlForServer(result.apiKeyUrls, serverName), }) }) .post( diff --git a/apps/server/src/lib/clients/klavis/klavis-client.ts b/apps/server/src/lib/clients/klavis/klavis-client.ts index 98a8a2a6e..1ff861af4 100644 --- a/apps/server/src/lib/clients/klavis/klavis-client.ts +++ b/apps/server/src/lib/clients/klavis/klavis-client.ts @@ -15,6 +15,19 @@ export interface StrataCreateResponse { apiKeyUrls?: Record } +interface KlavisIntegrationObject { + name?: string + isAuthenticated?: boolean + is_authenticated?: boolean +} + +type KlavisIntegrationItem = string | KlavisIntegrationObject + +export interface UserIntegration { + name: string + isAuthenticated: boolean +} + export class KlavisClient { private baseUrl: string @@ -81,19 +94,42 @@ export class KlavisClient { /** * Get user integrations with authentication status */ - async getUserIntegrations( - userId: string, - ): Promise> { + async getUserIntegrations(userId: string): Promise { const data = await this.request<{ - integrations: Array<{ name: string; isAuthenticated: boolean }> + integrations?: KlavisIntegrationItem[] }>('GET', `/user/${userId}/integrations`) - return data.integrations || [] + const integrations = Array.isArray(data.integrations) + ? data.integrations + : [] + return integrations + .map((integration) => this.normalizeIntegration(integration)) + .filter((integration): integration is UserIntegration => + Boolean(integration), + ) + } + + private normalizeIntegration( + integration: KlavisIntegrationItem, + ): UserIntegration | null { + if (typeof integration === 'string') { + return { name: integration, isAuthenticated: true } + } + const name = integration.name + if (!name || typeof name !== 'string') { + return null + } + const isAuthenticated = + typeof integration.isAuthenticated === 'boolean' + ? integration.isAuthenticated + : typeof integration.is_authenticated === 'boolean' + ? integration.is_authenticated + : false + return { name, isAuthenticated } } /** * Submit an API key to Klavis's set-auth endpoint via the proxy. * Extracts instanceId from the apiKeyUrl and routes through the proxy. - * Docs: POST /mcp-server/instance/set-auth with { instanceId, authData } */ async submitApiKey(apiKeyUrl: string, apiKey: string): Promise { const parsedUrl = new URL(apiKeyUrl) @@ -102,15 +138,11 @@ export class KlavisClient { throw new Error('Missing instance_id in apiKeyUrl') } - const data = await this.request<{ success: boolean; message?: string }>( - 'POST', - '/mcp-server/instance/set-auth', - { instanceId, authData: { api_key: apiKey } }, - ) - - if (!data.success) { - throw new Error(data.message || 'Klavis API key submission failed') - } + // request() already throws on non-2xx responses + await this.request('POST', '/mcp-server/instance/set-auth', { + instanceId, + authData: { api_key: apiKey }, + }) } /** diff --git a/apps/server/src/lib/clients/klavis/oauth-mcp-servers.ts b/apps/server/src/lib/clients/klavis/oauth-mcp-servers.ts index 59e8e4206..c9375c3e1 100644 --- a/apps/server/src/lib/clients/klavis/oauth-mcp-servers.ts +++ b/apps/server/src/lib/clients/klavis/oauth-mcp-servers.ts @@ -30,5 +30,34 @@ export const OAUTH_MCP_SERVERS: OAuthMcpServer[] = [ { name: 'Figma', description: 'Access and manage design files' }, { name: 'Canva', description: 'Create and manage designs' }, { name: 'Salesforce', description: 'Manage leads, contacts, opportunities' }, + { name: 'ClickUp', description: 'Manage tasks, projects, and workflows' }, + { name: 'Asana', description: 'Organize and track team projects' }, + { name: 'Monday', description: 'Manage work and team collaboration' }, + { name: 'Microsoft Teams', description: 'Chat, meet, and collaborate' }, + { name: 'Outlook Mail', description: 'Send, read, and manage emails' }, + { name: 'Outlook Calendar', description: 'Schedule meetings and events' }, + { name: 'Supabase', description: 'Manage databases and backend services' }, + { name: 'Vercel', description: 'Deploy and manage web applications' }, + { name: 'Postman', description: 'Test and manage APIs' }, + { name: 'Stripe', description: 'Manage payments and subscriptions' }, + { name: 'Cloudflare', description: 'Manage domains, DNS, and security' }, + { name: 'Brave Search', description: 'Search the web privately' }, + { name: 'Mem0', description: 'Store and retrieve AI memory' }, + { name: 'Exa', description: 'AI-powered semantic web search' }, + { name: 'Dropbox', description: 'Store and share files in the cloud' }, + { name: 'OneDrive', description: 'Store and sync files with Microsoft' }, + { name: 'WordPress', description: 'Manage websites and blog content' }, + { name: 'YouTube', description: 'Access video info and transcripts' }, + { name: 'Box', description: 'Manage and share enterprise files' }, + { name: 'HubSpot', description: 'Manage contacts, deals, and marketing' }, { name: 'PostHog', description: 'Query analytics, manage feature flags' }, + { name: 'Mixpanel', description: 'Analyze user behavior and metrics' }, + { name: 'Discord', description: 'Send messages and manage servers' }, + { name: 'WhatsApp', description: 'Send messages and manage conversations' }, + { name: 'Shopify', description: 'Manage products, orders, and store' }, + { name: 'Cal.com', description: 'Schedule meetings and manage availability' }, + { name: 'Resend', description: 'Send transactional and marketing emails' }, + { name: 'Google Forms', description: 'Create and manage forms and surveys' }, + { name: 'Zendesk', description: 'Manage support tickets and customers' }, + { name: 'Intercom', description: 'Manage customer messaging and support' }, ] diff --git a/apps/server/tests/api/routes/klavis.test.ts b/apps/server/tests/api/routes/klavis.test.ts new file mode 100644 index 000000000..bbe7a4f15 --- /dev/null +++ b/apps/server/tests/api/routes/klavis.test.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2025 BrowserOS + */ + +import { afterEach, describe, it } from 'bun:test' +import assert from 'node:assert' +import { createKlavisRoutes } from '../../../src/api/routes/klavis' + +const originalFetch = globalThis.fetch + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +describe('createKlavisRoutes', () => { + it('normalizes string integrations into authenticated entries', async () => { + globalThis.fetch = (async () => + Response.json({ + integrations: ['Google Docs', 'Slack'], + })) as typeof fetch + + const route = createKlavisRoutes({ browserosId: 'user-123' }) + const response = await route.request('/user-integrations') + const body = await response.json() + + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(body, { + integrations: [ + { name: 'Google Docs', is_authenticated: true }, + { name: 'Slack', is_authenticated: true }, + ], + count: 2, + }) + }) + + it('supports object integrations with mixed auth flag formats', async () => { + globalThis.fetch = (async () => + Response.json({ + integrations: [ + { name: 'Google Docs', isAuthenticated: false }, + { name: 'Slack', is_authenticated: true }, + ], + })) as typeof fetch + + const route = createKlavisRoutes({ browserosId: 'user-123' }) + const response = await route.request('/user-integrations') + const body = await response.json() + + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(body, { + integrations: [ + { name: 'Google Docs', is_authenticated: false }, + { name: 'Slack', is_authenticated: true }, + ], + count: 2, + }) + }) + + it('resolves auth URLs with normalized server name keys', async () => { + globalThis.fetch = (async () => + Response.json({ + strataServerUrl: 'https://strata.example.com', + strataId: 'strata-123', + addedServers: ['Google Docs'], + oauthUrls: { google_docs: 'https://oauth.example.com/google-docs' }, + apiKeyUrls: { + google_docs: 'https://auth.example.com/setup?instance_id=abc123', + }, + })) as typeof fetch + + const route = createKlavisRoutes({ browserosId: 'user-123' }) + const response = await route.request('/servers/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ serverName: 'Google Docs' }), + }) + const body = await response.json() + + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(body, { + success: true, + serverName: 'Google Docs', + strataId: 'strata-123', + addedServers: ['Google Docs'], + oauthUrl: 'https://oauth.example.com/google-docs', + apiKeyUrl: 'https://auth.example.com/setup?instance_id=abc123', + }) + }) +})