Skip to content

Commit 358aa11

Browse files
committed
feat(web): add prefab MCP connector presets
1 parent 1c75803 commit 358aa11

8 files changed

Lines changed: 136 additions & 8 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use client';
22

3+
// @techdebt : this seems to be an unused copy
4+
35
import { useMemo, useState } from "react";
46
import {
57
Command,
68
CommandGroup,
79
CommandInput,
810
CommandItem,
911
CommandList,
10-
CommandSeparator,
1112
} from "@/components/ui/command";
1213
import { Button } from "@/components/ui/button";
1314
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -110,8 +111,7 @@ export function PrefabConnectorPopover({
110111
</div>
111112
)}
112113
</CommandGroup>
113-
<CommandSeparator />
114-
<CommandGroup>
114+
<CommandGroup className="sticky bottom-0 z-10 border-t bg-popover">
115115
<CommandItem
116116
value="Custom URL"
117117
onSelect={handleSelectCustomUrl}

packages/web/src/ee/features/chat/mcp/components/prefabConnectorPopover.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
CommandInput,
88
CommandItem,
99
CommandList,
10-
CommandSeparator,
1110
} from "@/components/ui/command";
1211
import { Button } from "@/components/ui/button";
1312
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -110,8 +109,7 @@ export function PrefabConnectorPopover({
110109
</div>
111110
)}
112111
</CommandGroup>
113-
<CommandSeparator />
114-
<CommandGroup>
112+
<CommandGroup className="sticky bottom-0 z-10 border-t bg-popover">
115113
<CommandItem
116114
value="Custom URL"
117115
onSelect={handleSelectCustomUrl}

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,46 @@ describe('prefab MCP servers', () => {
1313
name: 'Atlassian',
1414
serverUrl: 'https://mcp.atlassian.com/v1/mcp/authv2',
1515
},
16+
{
17+
id: 'betterstack',
18+
name: 'Better Stack',
19+
serverUrl: 'https://mcp.betterstack.com',
20+
},
21+
{
22+
id: 'circleback',
23+
name: 'Circleback',
24+
serverUrl: 'https://circleback.ai/api/mcp',
25+
},
26+
{
27+
id: 'github',
28+
name: 'GitHub',
29+
serverUrl: 'https://api.githubcopilot.com/mcp/',
30+
},
31+
{
32+
id: 'gitlab',
33+
name: 'GitLab',
34+
serverUrl: 'https://gitlab.com/api/v4/mcp',
35+
},
1636
{
1737
id: 'linear',
1838
name: 'Linear',
1939
serverUrl: 'https://mcp.linear.app/mcp',
2040
},
41+
{
42+
id: 'notion',
43+
name: 'Notion',
44+
serverUrl: 'https://mcp.notion.com/mcp',
45+
},
2146
{
2247
id: 'posthog',
2348
name: 'PostHog',
2449
serverUrl: 'https://mcp.posthog.com/mcp',
2550
},
51+
{
52+
id: 'sentry',
53+
name: 'Sentry',
54+
serverUrl: 'https://mcp.sentry.dev/mcp',
55+
},
2656
{
2757
id: 'slack',
2858
name: 'Slack',
@@ -40,13 +70,39 @@ describe('prefab MCP servers', () => {
4070
test('hides already configured prefab servers after URL normalization', () => {
4171
const availableServers = getAvailablePrefabMcpServers(['https://mcp.slack.com/mcp/']);
4272

43-
expect(availableServers.map((server) => server.id)).toEqual(['atlassian', 'linear', 'posthog']);
73+
expect(availableServers.map((server) => server.id)).toEqual([
74+
'atlassian',
75+
'betterstack',
76+
'circleback',
77+
'github',
78+
'gitlab',
79+
'linear',
80+
'notion',
81+
'posthog',
82+
'sentry',
83+
]);
4484
});
4585

4686
test('hides the Atlassian prefab entry when the shared endpoint is configured', () => {
4787
const availableServers = getAvailablePrefabMcpServers(['https://mcp.atlassian.com/v1/mcp/authv2/']);
4888

49-
expect(availableServers.map((server) => server.id)).toEqual(['linear', 'posthog', 'slack']);
89+
expect(availableServers.map((server) => server.id)).toEqual([
90+
'betterstack',
91+
'circleback',
92+
'github',
93+
'gitlab',
94+
'linear',
95+
'notion',
96+
'posthog',
97+
'sentry',
98+
'slack',
99+
]);
100+
});
101+
102+
test('hides the GitHub prefab entry when the trailing slash is omitted', () => {
103+
const availableServers = getAvailablePrefabMcpServers(['https://api.githubcopilot.com/mcp']);
104+
105+
expect(availableServers.map((server) => server.id)).not.toContain('github');
50106
});
51107

52108
test('normalizes server URLs for duplicate comparisons', () => {

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,46 @@ const prefabMcpServers = [
1616
name: "Atlassian",
1717
serverUrl: "https://mcp.atlassian.com/v1/mcp/authv2",
1818
},
19+
{
20+
id: "betterstack",
21+
name: "Better Stack",
22+
serverUrl: "https://mcp.betterstack.com",
23+
},
24+
{
25+
id: "circleback",
26+
name: "Circleback",
27+
serverUrl: "https://circleback.ai/api/mcp",
28+
},
29+
{
30+
id: "github",
31+
name: "GitHub",
32+
serverUrl: "https://api.githubcopilot.com/mcp/",
33+
},
34+
{
35+
id: "gitlab",
36+
name: "GitLab",
37+
serverUrl: "https://gitlab.com/api/v4/mcp",
38+
},
1939
{
2040
id: "linear",
2141
name: "Linear",
2242
serverUrl: "https://mcp.linear.app/mcp",
2343
},
44+
{
45+
id: "notion",
46+
name: "Notion",
47+
serverUrl: "https://mcp.notion.com/mcp",
48+
},
2449
{
2550
id: "posthog",
2651
name: "PostHog",
2752
serverUrl: "https://mcp.posthog.com/mcp",
2853
},
54+
{
55+
id: "sentry",
56+
name: "Sentry",
57+
serverUrl: "https://mcp.sentry.dev/mcp",
58+
},
2959
{
3060
id: "slack",
3161
name: "Slack",

packages/web/src/ee/features/chat/mcp/utils.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ describe('getMcpFaviconUrl', () => {
4444
expect(getMcpFaviconUrl('https://mcp.atlassian.com/v1/mcp/authv2', 'Atlassian')).toMatch(/^data:image\/svg\+xml,/);
4545
});
4646

47+
test('returns known vendor favicon URLs for prefab servers with non-canonical MCP domains', () => {
48+
expect(getMcpFaviconUrl('https://mcp.betterstack.com', 'Better Stack')).toBe('https://betterstack.com/favicon.ico');
49+
expect(getMcpFaviconUrl('https://mcp.notion.com/mcp', 'Notion')).toBe('https://www.notion.com/front-static/favicon.ico');
50+
});
51+
52+
test('returns a local GitHub icon for the GitHub prefab server', () => {
53+
expect(getMcpFaviconUrl('https://api.githubcopilot.com/mcp/', 'GitHub')).toMatch(/^data:image\/svg\+xml,/);
54+
});
55+
4756
test('returns undefined for a malformed server URL', () => {
4857
expect(getMcpFaviconUrl('not a url')).toBeUndefined();
4958
});

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* to key favicon maps. Must be kept consistent everywhere — collisions on
77
* this value are prevented at server-creation time.
88
*/
9+
// @techdebt : duplicated in ee and non-ee
910
export function sanitizeMcpServerName(name: string): string {
1011
return name.toLowerCase().replace(/[^a-z0-9]/g, '_');
1112
}
1213

14+
// @techdebt : duplicated in ee and non-ee
1315
export function pluralize(count: number, singular: string, plural = `${singular}s`) {
1416
return count === 1 ? singular : plural;
1517
}
@@ -20,13 +22,15 @@ const compactNumberFormatter = new Intl.NumberFormat(undefined, {
2022
maximumFractionDigits: 1,
2123
});
2224

25+
// @techdebt : duplicated in ee and non-ee
2326
export function formatCount(count: number) {
2427
if (count >= 10_000) {
2528
return compactNumberFormatter.format(count);
2629
}
2730
return standardNumberFormatter.format(count);
2831
}
2932

33+
// @techdebt : duplicated in ee and non-ee
3034
export function formatUsageSharePercent(percent: number) {
3135
if (percent <= 0) {
3236
return "0%";
@@ -60,10 +64,19 @@ const atlassianIconSvg = `<svg width="48" height="48" viewBox="0 0 48 48" fill="
6064
</defs>
6165
</svg>`;
6266

67+
const githubIconSvg = `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
68+
<circle cx="16" cy="16" r="16" fill="white"/>
69+
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16 0 0 7.16 0 16C0 23.08 4.58 29.06 10.94 31.18C11.74 31.32 12.04 30.84 12.04 30.42C12.04 30.04 12.02 28.78 12.02 27.44C8 28.18 6.96 26.46 6.64 25.56C6.46 25.1 5.68 23.68 5 23.3C4.44 23 3.64 22.26 4.98 22.24C6.24 22.22 7.14 23.4 7.44 23.88C8.88 26.3 11.18 25.62 12.1 25.2C12.24 24.16 12.66 23.46 13.12 23.06C9.56 22.66 5.84 21.28 5.84 15.16C5.84 13.42 6.46 11.98 7.48 10.86C7.32 10.46 6.76 8.82 7.64 6.62C7.64 6.62 8.98 6.2 12.04 8.26C13.32 7.9 14.68 7.72 16.04 7.72C17.4 7.72 18.76 7.9 20.04 8.26C23.1 6.18 24.44 6.62 24.44 6.62C25.32 8.82 24.76 10.46 24.6 10.86C25.62 11.98 26.24 13.4 26.24 15.16C26.24 21.3 22.5 22.66 18.94 23.06C19.52 23.56 20.02 24.52 20.02 26.02C20.02 28.16 20 29.88 20 30.42C20 30.84 20.3 31.34 21.1 31.18C27.42 29.06 32 23.06 32 16C32 7.16 24.84 0 16 0V0Z" fill="#24292E"/>
70+
</svg>`;
71+
6372
const knownMcpFaviconUrlsBySanitizedName: Record<string, string> = {
6473
atlassian: createMcpIconDataUri(atlassianIconSvg),
74+
better_stack: "https://betterstack.com/favicon.ico",
75+
github: createMcpIconDataUri(githubIconSvg),
76+
notion: "https://www.notion.com/front-static/favicon.ico",
6577
};
6678

79+
// @techdebt : duplicated in ee and non-ee
6780
export function getMcpFaviconUrl(serverUrl: string, serverName?: string): string | undefined {
6881
if (serverName) {
6982
const knownFaviconUrl = knownMcpFaviconUrlsBySanitizedName[sanitizeMcpServerName(serverName)];

packages/web/src/features/chat/mcp/utils.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ describe('getMcpFaviconUrl', () => {
4444
expect(getMcpFaviconUrl('https://mcp.atlassian.com/v1/mcp/authv2', 'Atlassian')).toMatch(/^data:image\/svg\+xml,/);
4545
});
4646

47+
test('returns known vendor favicon URLs for prefab servers with non-canonical MCP domains', () => {
48+
expect(getMcpFaviconUrl('https://mcp.betterstack.com', 'Better Stack')).toBe('https://betterstack.com/favicon.ico');
49+
expect(getMcpFaviconUrl('https://mcp.notion.com/mcp', 'Notion')).toBe('https://www.notion.com/front-static/favicon.ico');
50+
});
51+
52+
test('returns a local GitHub icon for the GitHub prefab server', () => {
53+
expect(getMcpFaviconUrl('https://api.githubcopilot.com/mcp/', 'GitHub')).toMatch(/^data:image\/svg\+xml,/);
54+
});
55+
4756
test('returns undefined for a malformed server URL', () => {
4857
expect(getMcpFaviconUrl('not a url')).toBeUndefined();
4958
});

packages/web/src/features/chat/mcp/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* to key favicon maps. Must be kept consistent everywhere — collisions on
77
* this value are prevented at server-creation time.
88
*/
9+
// @techdebt : duplicated in ee and non-ee
910
export function sanitizeMcpServerName(name: string): string {
1011
return name.toLowerCase().replace(/[^a-z0-9]/g, '_');
1112
}
1213

14+
// @techdebt : duplicated in ee and non-ee
1315
export function pluralize(count: number, singular: string, plural = `${singular}s`) {
1416
return count === 1 ? singular : plural;
1517
}
@@ -20,13 +22,15 @@ const compactNumberFormatter = new Intl.NumberFormat(undefined, {
2022
maximumFractionDigits: 1,
2123
});
2224

25+
// @techdebt : duplicated in ee and non-ee
2326
export function formatCount(count: number) {
2427
if (count >= 10_000) {
2528
return compactNumberFormatter.format(count);
2629
}
2730
return standardNumberFormatter.format(count);
2831
}
2932

33+
// @techdebt : duplicated in ee and non-ee
3034
export function formatUsageSharePercent(percent: number) {
3135
if (percent <= 0) {
3236
return "0%";
@@ -60,10 +64,19 @@ const atlassianIconSvg = `<svg width="48" height="48" viewBox="0 0 48 48" fill="
6064
</defs>
6165
</svg>`;
6266

67+
const githubIconSvg = `<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
68+
<circle cx="16" cy="16" r="16" fill="white"/>
69+
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16 0 0 7.16 0 16C0 23.08 4.58 29.06 10.94 31.18C11.74 31.32 12.04 30.84 12.04 30.42C12.04 30.04 12.02 28.78 12.02 27.44C8 28.18 6.96 26.46 6.64 25.56C6.46 25.1 5.68 23.68 5 23.3C4.44 23 3.64 22.26 4.98 22.24C6.24 22.22 7.14 23.4 7.44 23.88C8.88 26.3 11.18 25.62 12.1 25.2C12.24 24.16 12.66 23.46 13.12 23.06C9.56 22.66 5.84 21.28 5.84 15.16C5.84 13.42 6.46 11.98 7.48 10.86C7.32 10.46 6.76 8.82 7.64 6.62C7.64 6.62 8.98 6.2 12.04 8.26C13.32 7.9 14.68 7.72 16.04 7.72C17.4 7.72 18.76 7.9 20.04 8.26C23.1 6.18 24.44 6.62 24.44 6.62C25.32 8.82 24.76 10.46 24.6 10.86C25.62 11.98 26.24 13.4 26.24 15.16C26.24 21.3 22.5 22.66 18.94 23.06C19.52 23.56 20.02 24.52 20.02 26.02C20.02 28.16 20 29.88 20 30.42C20 30.84 20.3 31.34 21.1 31.18C27.42 29.06 32 23.06 32 16C32 7.16 24.84 0 16 0V0Z" fill="#24292E"/>
70+
</svg>`;
71+
6372
const knownMcpFaviconUrlsBySanitizedName: Record<string, string> = {
6473
atlassian: createMcpIconDataUri(atlassianIconSvg),
74+
better_stack: "https://betterstack.com/favicon.ico",
75+
github: createMcpIconDataUri(githubIconSvg),
76+
notion: "https://www.notion.com/front-static/favicon.ico",
6577
};
6678

79+
// @techdebt : duplicated in ee and non-ee
6780
export function getMcpFaviconUrl(serverUrl: string, serverName?: string): string | undefined {
6881
if (serverName) {
6982
const knownFaviconUrl = knownMcpFaviconUrlsBySanitizedName[sanitizeMcpServerName(serverName)];

0 commit comments

Comments
 (0)