Skip to content

Commit f142109

Browse files
authored
feat(web): Slack-specific OAuth credentials copy in the connectors dialog (#1254)
1 parent 39434bb commit f142109

7 files changed

Lines changed: 81 additions & 8 deletions

File tree

packages/web/src/app/(app)/settings/workspaceAskAgent/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { hasEntitlement } from "@/lib/entitlements";
22
import { authenticatedPage } from "@/middleware/authenticatedPage";
33
import { OrgRole } from "@sourcebot/db";
4+
import { getMcpOAuthCallbackUrl } from "@/ee/features/chat/mcp/utils.server";
45
import { WorkspaceAskAgentPage } from "./workspaceAskAgentPage";
56
import { WorkspaceAskAgentEntitlementMessage } from "./workspaceAskAgentEntitlementMessage";
67

@@ -36,6 +37,7 @@ export default authenticatedPage<PageProps>(async ({ org, prisma }, { searchPara
3637
callbackStatus={status}
3738
callbackServer={server}
3839
callbackMessage={message}
40+
oauthRedirectUrl={getMcpOAuthCallbackUrl()}
3941
/>
4042
);
4143
}, { minRole: OrgRole.OWNER, redirectTo: '/settings' });

packages/web/src/app/(app)/settings/workspaceAskAgent/workspaceAskAgentPage.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import { cn, isServiceError } from "@/lib/utils";
3030
import { useQuery, useQueryClient } from "@tanstack/react-query";
3131
import { AlertTriangleIcon, CableIcon, CopyIcon, Loader2, MoreHorizontalIcon, PlusIcon, Trash2Icon } from "lucide-react";
3232
import { PrefabConnectorPopover } from "@/ee/features/chat/mcp/components/prefabConnectorPopover";
33-
import type { PrefabMcpServer } from "@/ee/features/chat/mcp/prefabMcpServers";
33+
import Markdown from "react-markdown";
34+
import { getStaticOAuthDescription, type PrefabMcpServer } from "@/ee/features/chat/mcp/prefabMcpServers";
3435
import type { McpConfigurationServer, ServerToolsEntry } from "@/ee/features/chat/mcp/types";
3536

3637
function clearCallbackParams() {
@@ -45,6 +46,7 @@ interface WorkspaceAskAgentPageProps {
4546
callbackStatus?: string;
4647
callbackServer?: string;
4748
callbackMessage?: string;
49+
oauthRedirectUrl: string;
4850
}
4951

5052
type WorkspaceConnectorStatus = {
@@ -158,7 +160,7 @@ function WorkspaceConnectorCard({
158160
);
159161
}
160162

161-
export function WorkspaceAskAgentPage({ callbackStatus, callbackServer, callbackMessage }: WorkspaceAskAgentPageProps) {
163+
export function WorkspaceAskAgentPage({ callbackStatus, callbackServer, callbackMessage, oauthRedirectUrl }: WorkspaceAskAgentPageProps) {
162164
const { toast } = useToast();
163165
const queryClient = useQueryClient();
164166
const router = useRouter();
@@ -581,8 +583,42 @@ export function WorkspaceAskAgentPage({ callbackStatus, callbackServer, callback
581583
<DialogContent className="sm:max-w-md">
582584
<DialogHeader>
583585
<DialogTitle>OAuth Client Credentials Required</DialogTitle>
584-
<DialogDescription>
585-
This connector does not advertise dynamic client registration. Provide OAuth client credentials from a pre-registered app before members can connect to it.
586+
<DialogDescription asChild>
587+
<div className="text-sm text-muted-foreground">
588+
<Markdown
589+
components={{
590+
p: ({ children }) => <p className="[&:not(:first-child)]:mt-2">{children}</p>,
591+
a: ({ children, href }) => (
592+
<a
593+
href={href}
594+
target="_blank"
595+
rel="noopener noreferrer"
596+
className="text-link hover:underline"
597+
>
598+
{children}
599+
</a>
600+
),
601+
code: ({ children }) => (
602+
<span className="inline-flex items-center gap-1 align-middle">
603+
<code className="bg-muted rounded px-1 py-0.5 text-xs break-all">{children}</code>
604+
<button
605+
type="button"
606+
aria-label="Copy redirect URL"
607+
className="text-muted-foreground hover:text-foreground"
608+
onClick={() => {
609+
navigator.clipboard.writeText(String(children));
610+
toast({ title: "Copied", description: "Redirect URL copied to clipboard." });
611+
}}
612+
>
613+
<CopyIcon className="h-3 w-3" />
614+
</button>
615+
</span>
616+
),
617+
}}
618+
>
619+
{getStaticOAuthDescription(pendingClientCredentialsServer?.serverUrl ?? "", oauthRedirectUrl)}
620+
</Markdown>
621+
</div>
586622
</DialogDescription>
587623
</DialogHeader>
588624
<div className="space-y-4 py-4">

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { env, createLogger } from '@sourcebot/shared';
44
import { hasEntitlement } from '@/lib/entitlements';
55
import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants';
66
import { PrismaOAuthClientProvider } from '@/ee/features/chat/mcp/prismaOAuthClientProvider';
7+
import { getMcpOAuthCallbackUrl } from '@/ee/features/chat/mcp/utils.server';
78
// Note: We use the raw (unscoped) prisma client here because this route handles OAuth
89
// redirect callbacks from external providers, so it can't go through withAuth. Session
910
// identity is verified via NextAuth's auth() instead, and all queries filter by userId.
@@ -166,7 +167,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
166167
serverId: userServer.serverId,
167168
orgId: userServer.server.orgId,
168169
userId: session.user.id,
169-
callbackUrl: `${env.AUTH_URL}/api/ee/askmcp/callback`,
170+
callbackUrl: getMcpOAuthCallbackUrl(),
170171
});
171172

172173
let result: Awaited<ReturnType<typeof mcpAuth>>;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ConnectMcpResponse } from "@/app/api/(server)/ee/askmcp/connect/types";
1313
import { createLogger, env } from "@sourcebot/shared";
1414
import { __unsafePrisma } from '@/prisma';
1515
import { getExternalMcpErrorLogFields } from '@/ee/features/chat/mcp/externalMcpError';
16+
import { getMcpOAuthCallbackUrl } from '@/ee/features/chat/mcp/utils.server';
1617
import { ErrorCode } from '@/lib/errorCodes';
1718
import { StatusCodes } from 'http-status-codes';
1819
import { normalizeMcpOAuthReturnTo } from '@/ee/features/chat/mcp/mcpOAuthReturnTo';
@@ -130,7 +131,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
130131
serverId: mcpServer.id,
131132
orgId: org.id,
132133
userId: user.id,
133-
callbackUrl: `${env.AUTH_URL}/api/ee/askmcp/callback`,
134+
callbackUrl: getMcpOAuthCallbackUrl(),
134135
callbackReturnTo,
135136
allowClientRegistration: true,
136137
});

packages/web/src/ee/features/chat/mcp/mcpClientFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { createLogger, env } from '@sourcebot/shared';
1+
import { createLogger } from '@sourcebot/shared';
22
import { PrismaOAuthClientProvider } from '@/ee/features/chat/mcp/prismaOAuthClientProvider';
3+
import { getMcpOAuthCallbackUrl } from '@/ee/features/chat/mcp/utils.server';
34
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
45
import type { PrismaClient } from '@sourcebot/db';
56
import { getExternalMcpErrorLogFields } from './externalMcpError';
@@ -72,7 +73,7 @@ export async function getConnectedMcpClients(prisma: PrismaClient, userId: strin
7273
serverId: userServer.serverId,
7374
orgId,
7475
userId,
75-
callbackUrl: `${env.AUTH_URL}/api/ee/askmcp/callback`,
76+
callbackUrl: getMcpOAuthCallbackUrl(),
7677
});
7778

7879
const transport = new StreamableHTTPClientTransport(

packages/web/src/ee/features/chat/mcp/prefabMcpServers.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ export interface PrefabMcpServer {
22
id: string;
33
name: string;
44
serverUrl: string;
5+
descriptionOverride?: string;
56
}
67

8+
export const DEFAULT_STATIC_OAUTH_DESCRIPTION =
9+
"This connector does not advertise dynamic client registration. Provide OAuth client credentials from a pre-registered app before members can connect to it.";
10+
11+
export const OAUTH_REDIRECT_URL_PLACEHOLDER = "{{REDIRECT_URL}}";
12+
713
const prefabMcpServers = [
814
{
915
id: "atlassian",
@@ -24,6 +30,8 @@ const prefabMcpServers = [
2430
id: "slack",
2531
name: "Slack",
2632
serverUrl: "https://mcp.slack.com/mcp",
33+
descriptionOverride:
34+
"Slack doesn't support dynamic client registration, so you'll need to create an app in your Slack workspace and provide Sourcebot a Client ID and Secret. These are stored encrypted within your Sourcebot deployment.\n\nVisit [this page](https://api.slack.com/apps) to create a Slack app. Set the redirect URL (under OAuth & Permissions) to `{{REDIRECT_URL}}`",
2735
},
2836
] satisfies PrefabMcpServer[];
2937

@@ -41,6 +49,21 @@ export function normalizeMcpServerUrlForComparison(serverUrl: string): string {
4149
}
4250
}
4351

52+
export function getStaticOAuthDescription(serverUrl: string, redirectUrl?: string): string {
53+
const normalizedServerUrl = normalizeMcpServerUrlForComparison(serverUrl);
54+
const prefabServer = PREFAB_MCP_SERVERS.find((server) => (
55+
normalizeMcpServerUrlForComparison(server.serverUrl) === normalizedServerUrl
56+
));
57+
58+
const description = prefabServer?.descriptionOverride ?? DEFAULT_STATIC_OAUTH_DESCRIPTION;
59+
60+
if (redirectUrl) {
61+
return description.replaceAll(OAUTH_REDIRECT_URL_PLACEHOLDER, redirectUrl);
62+
}
63+
64+
return description;
65+
}
66+
4467
export function getAvailablePrefabMcpServers(configuredServerUrls: string[]): PrefabMcpServer[] {
4568
const configuredServerUrlSet = new Set(
4669
configuredServerUrls.map((serverUrl) => normalizeMcpServerUrlForComparison(serverUrl)),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import 'server-only';
2+
3+
import { env } from '@sourcebot/shared';
4+
5+
export const MCP_OAUTH_CALLBACK_PATH = '/api/ee/askmcp/callback';
6+
7+
export function getMcpOAuthCallbackUrl(): string {
8+
return `${env.AUTH_URL}${MCP_OAUTH_CALLBACK_PATH}`;
9+
}

0 commit comments

Comments
 (0)