Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/connect-your-ai-assistant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@hyperdx/app": patch
---

feat: add a "Connect your AI assistant" section to Team Settings

A new section on the Team Settings page (Integrations tab, above the API Keys
card) lets a user install the HyperDX MCP server in Claude Code, Cursor,
VS Code + Copilot, Codex CLI, or any MCP-compatible host without hand-rolling
JSON. Per-host snippets carry the user's personal access key so the install
works against the existing `/api/mcp` route without extra setup.
Comment on lines +8 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Changeset description references the wrong tab

The note says the section lives on the "Integrations tab, above the API Keys card", but the actual implementation introduces a dedicated "API & Agents" tab and removes the API Keys card from Integrations entirely. The user-facing change note will be misleading in the changelog.

Suggested change
card) lets a user install the HyperDX MCP server in Claude Code, Cursor,
VS Code + Copilot, Codex CLI, or any MCP-compatible host without hand-rolling
JSON. Per-host snippets carry the user's personal access key so the install
works against the existing `/api/mcp` route without extra setup.
A new "API & Agents" tab on the Team Settings page lets a user install the
HyperDX MCP server in Claude Code, Cursor, VS Code + Copilot, Codex CLI,
OpenCode, or any MCP-compatible host without hand-rolling JSON. Per-host
snippets carry the user's personal access key so the install works against
the existing `/api/mcp` route without extra setup. The API Keys card has
moved to this tab alongside the new install panel.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

19 changes: 15 additions & 4 deletions packages/app/src/TeamPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { PageHeader } from './components/PageHeader';
import ApiKeysSection from './components/TeamSettings/ApiKeysSection';
import ConnectionsSection from './components/TeamSettings/ConnectionsSection';
import IntegrationsSection from './components/TeamSettings/IntegrationsSection';
import McpServerSection from './components/TeamSettings/McpServerSection';
import SecurityPoliciesSection from './components/TeamSettings/SecurityPoliciesSection';
import SourcesSection from './components/TeamSettings/SourcesSection';
import TeamMembersSection from './components/TeamSettings/TeamMembersSection';
Expand Down Expand Up @@ -131,6 +132,20 @@ export default function TeamPage() {
},
]
: []),
{
value: 'api-agents',
label: 'API & Agents',
sections: [
{
id: 'team-api-agents-api-keys',
content: <ApiKeysSection />,
},
{
id: 'team-api-agents-mcp-server',
content: <McpServerSection />,
},
],
},
{
value: 'integrations',
label: 'Integrations',
Expand All @@ -139,10 +154,6 @@ export default function TeamPage() {
id: 'team-integrations-webhooks',
content: <IntegrationsSection />,
},
{
id: 'team-integrations-api-keys',
content: <ApiKeysSection />,
},
],
},
{
Expand Down
49 changes: 49 additions & 0 deletions packages/app/src/components/ClickStackOnboarding/CopySnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Button, Code, CopyButton, Group, Stack, Text } from '@mantine/core';
import { IconCheck, IconCopy } from '@tabler/icons-react';

interface CopySnippetProps {
label: string;
snippet: string;
}

/**
* Pre-formatted snippet with a copy-to-clipboard button. The copy
* affordance handles its own `Copied` affirmation via Mantine's
* `<CopyButton>`.
*/
export function CopySnippet({ label, snippet }: CopySnippetProps) {
return (
<Stack gap="xs">
<Text size="sm" fw={500}>
{label}
</Text>
<Group align="flex-start" w="100%" gap="xs">
<Code
block
flex={1}
style={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
fontFamily: 'var(--mantine-font-family-monospace)',
}}
>
{snippet}
</Code>
<CopyButton value={snippet}>
{({ copied, copy }) => (
<Button
onClick={copy}
variant="subtle"
size="xs"
leftSection={
copied ? <IconCheck size={14} /> : <IconCopy size={14} />
}
>
{copied ? 'Copied' : 'Copy'}
</Button>
)}
</CopyButton>
</Group>
</Stack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ReactNode, useState } from 'react';
import { Anchor, Button, Collapse, Group, Stack, Tooltip } from '@mantine/core';

import { CopySnippet } from './CopySnippet';

interface DeeplinkInstallProps {
buttonLabel: string;
deeplink: string;
fallbackLabel: string;
fallbackSnippet: string;
note?: ReactNode;
}

/**
* One-click "Add to <host>" deep-link install for hosts that
* support it (Cursor, VS Code + Copilot). The manual fallback
* snippet stays tucked behind a `Manual setup` toggle so the
* primary affordance is always the deep link, with the JSON
* paste-it-yourself path available for users who can't or won't
* use the deep link.
*/
export function DeeplinkInstall({
buttonLabel,
deeplink,
fallbackLabel,
fallbackSnippet,
note,
}: DeeplinkInstallProps) {
const [manualOpen, setManualOpen] = useState(false);
return (
<Stack gap="xs">
<Group gap="sm" align="center">
<Tooltip
label="Opens the host with the server pre-configured"
withArrow
>
<Button component="a" href={deeplink} variant="primary">
{buttonLabel}
</Button>
</Tooltip>
<Anchor
component="button"
size="sm"
onClick={() => setManualOpen(v => !v)}
>
{manualOpen ? 'Hide manual setup' : 'Manual setup'}
</Anchor>
</Group>
{note}
<Collapse expanded={manualOpen} transitionDuration={150}>
<CopySnippet label={fallbackLabel} snippet={fallbackSnippet} />
</Collapse>
</Stack>
);
}
209 changes: 209 additions & 0 deletions packages/app/src/components/ClickStackOnboarding/McpInstallPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { useMemo, useState } from 'react';
import { Group, SegmentedControl, Stack, Text } from '@mantine/core';
import {
IconBraces,
IconBrandOpenai,
IconBrandVisualStudio,
IconCode,
IconRobot,
IconTerminal2,
} from '@tabler/icons-react';

import { CopySnippet } from './CopySnippet';
import { DeeplinkInstall } from './DeeplinkInstall';
import {
buildAllSnippets,
type BuiltSnippets,
type DeploymentShape,
} from './installSnippets';

/**
* Agent hosts the install panel covers. Five named installs (Claude
* Code, Cursor, VS Code, Codex CLI, OpenCode) plus "Other" as the
* JSON-fallback escape hatch for any other MCP-compatible host
* (Claude Desktop, Continue, Cline, ...).
*
* ChatGPT is intentionally absent: native MCP isn't there yet, and
* bridges are a user-side decision better tracked in the docs than
* in this UI surface.
*/
type AgentHost =
| 'claude-code'
| 'cursor'
| 'vscode-copilot'
| 'codex-cli'
| 'opencode'
| 'other';

interface HostChoice {
id: AgentHost;
label: string;
}

const CHOICES: HostChoice[] = [
{ id: 'claude-code', label: 'Claude Code' },
{ id: 'cursor', label: 'Cursor' },
{ id: 'vscode-copilot', label: 'VS Code' },
{ id: 'codex-cli', label: 'Codex CLI' },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on dropping Codex for OpenCode (biased maybe). I know we have several devs internally who use OpenCode so this would help with adoption

The config for OpenCOde is slightky different

    "clickstack": {
      "type": "remote", <- Note not type http
      "url": "http://localhost:xxx/api/mcp",
      "headers": {
        "Authorization": "Bearer xxx"
      }
    },

{ id: 'opencode', label: 'OpenCode' },
{ id: 'other', label: 'Other' },
];

const HOST_IDS = new Set<string>(CHOICES.map(c => c.id));

function isAgentHost(value: string): value is AgentHost {
return HOST_IDS.has(value);
}

interface McpInstallPanelProps {
/**
* Deployment shape derived from `useMe()` in the caller. The
* caller is responsible for not mounting this panel until the
* deployment is ready (matching the convention in
* `ApiKeysSection`), so the type here is non-nullable.
*/
deployment: DeploymentShape;
}

/**
* Renders the host picker plus the install primitive (CLI command,
* deep link, or JSON block) for the chosen host. Presentational;
* the deployment shape comes in via props so the same component
* can render from any surface that resolves a deployment + access
* key.
*
* The access key is inlined in the rendered snippet to match the
* existing API Keys card pattern, which shows the key in plain
* text. A follow-up will introduce a shared mask + reveal-to-copy
* affordance across every credential surface in Team Settings.
*/
export default function McpInstallPanel({ deployment }: McpInstallPanelProps) {
const [host, setHost] = useState<AgentHost>('claude-code');

const snippets = useMemo(() => buildAllSnippets(deployment), [deployment]);

return (
<Stack gap="md">
<SegmentedControl
fullWidth
value={host}
onChange={value => {
// Narrow the SegmentedControl's `string` callback against
// the CHOICES set so a future out-of-band value cannot
// silently install an invalid host. CHOICES is the source
// of truth for the option list.
if (isAgentHost(value)) {
setHost(value);
}
}}
data={CHOICES.map(c => ({
value: c.id,
label: (
<Group gap="xs" justify="center" wrap="nowrap">
<HostIcon id={c.id} />
<Text size="sm">{c.label}</Text>
</Group>
),
}))}
aria-label="MCP host"
/>

<HostInstall host={host} snippets={snippets} />
</Stack>
);
}

function HostIcon({ id }: { id: AgentHost }) {
switch (id) {
case 'claude-code':
return <IconTerminal2 size={16} />;
case 'cursor':
return <IconCode size={16} />;
case 'vscode-copilot':
return <IconBrandVisualStudio size={16} />;
case 'codex-cli':
return <IconBrandOpenai size={16} />;
case 'opencode':
return <IconBraces size={16} />;
case 'other':
return <IconRobot size={16} />;
}
// Exhaustiveness check via `satisfies never`: adding a new
// AgentHost variant without extending the switch fails the
// compile here. Defensive `return null` (instead of throwing)
// keeps a runtime-only unknown variant from crashing the panel.
id satisfies never;
return null;
}

interface HostInstallProps {
host: AgentHost;
snippets: BuiltSnippets;
}

function HostInstall({ host, snippets }: HostInstallProps) {
switch (host) {
case 'claude-code':
return (
<CopySnippet
label="Paste in your terminal:"
snippet={snippets.claudeCode}
/>
);

case 'cursor':
return (
<DeeplinkInstall
buttonLabel="Add to Cursor"
deeplink={snippets.cursor}
fallbackLabel="Or paste this JSON into Cursor settings > MCP:"
fallbackSnippet={snippets.jsonBlock}
/>
);

case 'vscode-copilot':
return (
<DeeplinkInstall
buttonLabel="Add to VS Code"
deeplink={snippets.vscode}
fallbackLabel="Or paste this JSON into .vscode/mcp.json:"
fallbackSnippet={snippets.jsonBlock}
note={
<Text size="xs" c="dimmed">
Requires VS Code 1.99+ with the Copilot Chat MCP feature enabled.
</Text>
}
/>
);

case 'codex-cli':
return (
<CopySnippet
label="Paste in your terminal:"
snippet={snippets.codexCli}
/>
);

case 'opencode':
return (
<CopySnippet
label="Paste this into `opencode.json` (project) or `~/.config/opencode/config.json` (global):"
snippet={snippets.openCode}
/>
);

case 'other':
return (
<CopySnippet
label="Paste this into your host's MCP config:"
snippet={snippets.jsonBlock}
/>
);
}
// Exhaustiveness check via `satisfies never`: adding a new
// AgentHost variant without extending the switch fails the
// compile here. Defensive `return null` (instead of throwing)
// keeps a runtime-only unknown variant from crashing the panel.
host satisfies never;
return null;
}
Loading
Loading