Skip to content

Commit 9836a5d

Browse files
Ripwordsclaude
andcommitted
feat(mcp): add /settings/mcp page (connect snippets + connected apps)
Adds the MCP settings page with copy-paste config snippets for Claude Desktop, Cursor, ChatGPT, and generic mcp-remote, plus a connected-apps list with per-client Disconnect buttons. Wired into the top-bar user menu and command palette so it's reachable without being admin-gated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d68b8a7 commit 9836a5d

3 files changed

Lines changed: 284 additions & 5 deletions

File tree

apps/dashboard/app/components/shell/app-top-bar.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const userItems = computed(() => [
1414
icon: "i-heroicons-user",
1515
to: "/settings/account",
1616
},
17+
{
18+
label: "AI assistants (MCP)",
19+
icon: "i-heroicons-cpu-chip",
20+
to: "/settings/mcp",
21+
},
1722
],
1823
[
1924
{

apps/dashboard/app/components/shell/command-palette.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,18 @@ const navGroup = computed<CommandGroup>(() => {
133133
)
134134
}
135135
136-
items.push({
137-
label: "Account",
138-
icon: "i-heroicons-user",
139-
onSelect: () => go("/settings/account"),
140-
})
136+
items.push(
137+
{
138+
label: "Account",
139+
icon: "i-heroicons-user",
140+
onSelect: () => go("/settings/account"),
141+
},
142+
{
143+
label: "AI assistants (MCP)",
144+
icon: "i-heroicons-cpu-chip",
145+
onSelect: () => go("/settings/mcp"),
146+
},
147+
)
141148
142149
if (isAdmin.value) {
143150
items.push(
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from "vue"
3+
4+
useHead({ title: "MCP / AI assistants" })
5+
6+
const toast = useToast()
7+
8+
// `useRequestURL()` resolves at request time (SSR → Host header via reverse
9+
// proxy; client → `window.location.origin`) so copy-paste snippets always
10+
// match the dashboard's real public hostname without relying on a build-time
11+
// env var.
12+
const origin = useRequestURL().origin
13+
const mcpUrl = computed(() => `${origin}/api/mcp`)
14+
15+
const claudeDesktopSnippet = computed(() =>
16+
JSON.stringify(
17+
{
18+
mcpServers: {
19+
repro: {
20+
command: "npx",
21+
args: ["-y", "mcp-remote", mcpUrl.value],
22+
},
23+
},
24+
},
25+
null,
26+
2,
27+
),
28+
)
29+
30+
const cursorSnippet = computed(() =>
31+
JSON.stringify(
32+
{
33+
mcpServers: {
34+
repro: { url: mcpUrl.value, transport: "streamable-http" },
35+
},
36+
},
37+
null,
38+
2,
39+
),
40+
)
41+
42+
const remoteCli = computed(() => `npx mcp-remote ${mcpUrl.value}`)
43+
44+
interface Connection {
45+
clientId: string
46+
clientName: string
47+
scopes: string[]
48+
connectedAt: string
49+
lastUsedAt: string | null
50+
}
51+
52+
const { data: connectionsData, refresh } = await useApi<{ connections: Connection[] }>(
53+
"/api/me/mcp-connections",
54+
)
55+
56+
const connections = computed(() => connectionsData.value?.connections ?? [])
57+
58+
const revokingClient = ref<string | null>(null)
59+
60+
async function revoke(clientId: string): Promise<void> {
61+
revokingClient.value = clientId
62+
try {
63+
await $fetch(`/api/me/mcp-connections/${clientId}`, {
64+
method: "DELETE",
65+
credentials: "include",
66+
})
67+
await refresh()
68+
toast.add({
69+
title: "Disconnected",
70+
color: "success",
71+
icon: "i-heroicons-check-circle",
72+
})
73+
} catch (e: unknown) {
74+
const err = e as { statusMessage?: string; message?: string }
75+
toast.add({
76+
title: "Could not disconnect",
77+
description: err.statusMessage ?? err.message ?? "Unknown error",
78+
color: "error",
79+
icon: "i-heroicons-exclamation-triangle",
80+
})
81+
} finally {
82+
revokingClient.value = null
83+
}
84+
}
85+
86+
async function copy(text: string): Promise<void> {
87+
try {
88+
await navigator.clipboard.writeText(text)
89+
toast.add({
90+
title: "Copied to clipboard",
91+
color: "success",
92+
icon: "i-heroicons-clipboard-document-check",
93+
})
94+
} catch {
95+
toast.add({ title: "Copy failed", color: "error" })
96+
}
97+
}
98+
</script>
99+
100+
<template>
101+
<div class="space-y-8 max-w-3xl">
102+
<header>
103+
<h1 class="text-2xl font-semibold text-default">MCP / AI assistants</h1>
104+
<p class="text-sm text-muted mt-1">
105+
Connect an AI assistant (Claude Desktop, Cursor, ChatGPT, …) to triage your Repro tickets
106+
through the Model Context Protocol.
107+
</p>
108+
</header>
109+
110+
<!-- ── Connect section ─────────────────────────────────────────────── -->
111+
<section class="space-y-4">
112+
<h2 class="text-lg font-semibold text-default">Connect an AI assistant</h2>
113+
114+
<!-- Claude Desktop -->
115+
<UCard>
116+
<template #header>
117+
<h3 class="text-base font-semibold text-default">Claude Desktop</h3>
118+
</template>
119+
<p class="text-sm text-muted mb-3">
120+
Add to
121+
<code class="font-mono px-1 rounded bg-muted">
122+
~/Library/Application Support/Claude/claude_desktop_config.json
123+
</code>
124+
(macOS) or
125+
<code class="font-mono px-1 rounded bg-muted">
126+
%APPDATA%\Claude\claude_desktop_config.json
127+
</code>
128+
(Windows):
129+
</p>
130+
<div class="relative rounded-lg border border-default overflow-hidden">
131+
<pre
132+
class="text-xs p-4 overflow-x-auto font-mono leading-relaxed"
133+
><code>{{ claudeDesktopSnippet }}</code></pre>
134+
<UButton
135+
class="absolute top-2 right-2"
136+
icon="i-heroicons-clipboard"
137+
size="xs"
138+
color="neutral"
139+
variant="subtle"
140+
aria-label="Copy Claude Desktop snippet"
141+
@click="copy(claudeDesktopSnippet)"
142+
/>
143+
</div>
144+
</UCard>
145+
146+
<!-- Cursor -->
147+
<UCard>
148+
<template #header>
149+
<h3 class="text-base font-semibold text-default">Cursor</h3>
150+
</template>
151+
<p class="text-sm text-muted mb-3">
152+
Add to
153+
<code class="font-mono px-1 rounded bg-muted">~/.cursor/mcp.json</code>:
154+
</p>
155+
<div class="relative rounded-lg border border-default overflow-hidden">
156+
<pre
157+
class="text-xs p-4 overflow-x-auto font-mono leading-relaxed"
158+
><code>{{ cursorSnippet }}</code></pre>
159+
<UButton
160+
class="absolute top-2 right-2"
161+
icon="i-heroicons-clipboard"
162+
size="xs"
163+
color="neutral"
164+
variant="subtle"
165+
aria-label="Copy Cursor snippet"
166+
@click="copy(cursorSnippet)"
167+
/>
168+
</div>
169+
</UCard>
170+
171+
<!-- ChatGPT custom connectors -->
172+
<UCard>
173+
<template #header>
174+
<h3 class="text-base font-semibold text-default">ChatGPT custom connectors</h3>
175+
</template>
176+
<p class="text-sm text-muted mb-3">
177+
Paste this URL into the connector dialog — OAuth discovery and login happen automatically:
178+
</p>
179+
<div class="flex items-center gap-2">
180+
<code
181+
class="flex-1 text-xs font-mono bg-muted px-3 py-2 rounded border border-default truncate"
182+
>
183+
{{ mcpUrl }}
184+
</code>
185+
<UButton
186+
icon="i-heroicons-clipboard"
187+
size="xs"
188+
color="neutral"
189+
variant="subtle"
190+
aria-label="Copy MCP URL"
191+
@click="copy(mcpUrl)"
192+
>
193+
Copy
194+
</UButton>
195+
</div>
196+
</UCard>
197+
198+
<!-- Generic / mcp-remote -->
199+
<UCard>
200+
<template #header>
201+
<h3 class="text-base font-semibold text-default">Generic (any MCP client)</h3>
202+
</template>
203+
<p class="text-sm text-muted mb-3">
204+
Use the <code class="font-mono px-1 rounded bg-muted">mcp-remote</code> shim to bridge any
205+
client that only supports the local stdio transport:
206+
</p>
207+
<div class="flex items-center gap-2">
208+
<code
209+
class="flex-1 text-xs font-mono bg-muted px-3 py-2 rounded border border-default truncate"
210+
>
211+
{{ remoteCli }}
212+
</code>
213+
<UButton
214+
icon="i-heroicons-clipboard"
215+
size="xs"
216+
color="neutral"
217+
variant="subtle"
218+
aria-label="Copy mcp-remote command"
219+
@click="copy(remoteCli)"
220+
>
221+
Copy
222+
</UButton>
223+
</div>
224+
</UCard>
225+
</section>
226+
227+
<!-- ── Connected apps section ───────────────────────────────────────── -->
228+
<section class="space-y-4">
229+
<h2 class="text-lg font-semibold text-default">Connected apps</h2>
230+
231+
<p v-if="connections.length === 0" class="text-sm text-muted">
232+
No AI assistants connected yet. Follow the instructions above to connect your first client.
233+
</p>
234+
235+
<ul v-else class="space-y-2">
236+
<li
237+
v-for="c in connections"
238+
:key="c.clientId"
239+
class="flex items-start justify-between gap-4 rounded-lg border border-default p-4"
240+
>
241+
<div class="space-y-1 min-w-0">
242+
<div class="font-medium text-default text-sm">{{ c.clientName }}</div>
243+
<div class="text-xs text-muted">
244+
Connected {{ new Date(c.connectedAt).toLocaleDateString() }}
245+
<span v-if="c.lastUsedAt">
246+
&middot; last used {{ new Date(c.lastUsedAt).toLocaleString() }}
247+
</span>
248+
</div>
249+
<div class="text-xs text-muted">
250+
Scopes:
251+
<span class="font-mono">{{ c.scopes.length ? c.scopes.join(", ") : "(none)" }}</span>
252+
</div>
253+
</div>
254+
<UButton
255+
size="xs"
256+
variant="subtle"
257+
color="error"
258+
:loading="revokingClient === c.clientId"
259+
@click="revoke(c.clientId)"
260+
>
261+
Disconnect
262+
</UButton>
263+
</li>
264+
</ul>
265+
</section>
266+
</div>
267+
</template>

0 commit comments

Comments
 (0)