Skip to content

Commit d73a7f3

Browse files
tanviet12claude
andcommitted
fix: add redirect_uris and scopes to MCP client creation
- Frontend: add combobox for redirect URIs and select for scopes - Frontend: display redirect URIs in client list table - Backend: accept scopes from request instead of hardcoding - Backend: return redirect_uris in ListMCPClients response - Fixes Claude.ai MCP connect failing with invalid_redirect_uri Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 14e7967 commit d73a7f3

2 files changed

Lines changed: 80 additions & 17 deletions

File tree

backend/mcp/oauth.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -542,20 +542,22 @@ func ListMCPClients(c *gin.Context) {
542542
db.DB.Where("user_id = ?", userID).Order("created_at DESC").Find(&clients)
543543

544544
type clientResponse struct {
545-
ID string `json:"id"`
546-
ClientID string `json:"client_id"`
547-
Name string `json:"name"`
548-
Scopes string `json:"scopes"`
549-
CreatedAt time.Time `json:"created_at"`
545+
ID string `json:"id"`
546+
ClientID string `json:"client_id"`
547+
Name string `json:"name"`
548+
RedirectURIs string `json:"redirect_uris"`
549+
Scopes string `json:"scopes"`
550+
CreatedAt time.Time `json:"created_at"`
550551
}
551552
results := make([]clientResponse, len(clients))
552553
for i, cl := range clients {
553554
results[i] = clientResponse{
554-
ID: cl.ID,
555-
ClientID: cl.ClientID,
556-
Name: cl.Name,
557-
Scopes: cl.Scopes,
558-
CreatedAt: cl.CreatedAt,
555+
ID: cl.ID,
556+
ClientID: cl.ClientID,
557+
Name: cl.Name,
558+
RedirectURIs: cl.RedirectURIs,
559+
Scopes: cl.Scopes,
560+
CreatedAt: cl.CreatedAt,
559561
}
560562
}
561563
c.JSON(http.StatusOK, results)
@@ -566,6 +568,7 @@ func CreateMCPClient(c *gin.Context) {
566568
var req struct {
567569
Name string `json:"name" binding:"required"`
568570
RedirectURIs []string `json:"redirect_uris"`
571+
Scopes []string `json:"scopes"`
569572
}
570573
if err := c.ShouldBindJSON(&req); err != nil {
571574
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
@@ -586,13 +589,26 @@ func CreateMCPClient(c *gin.Context) {
586589
redirectURIsJSON = string(b)
587590
}
588591

592+
// Validate and set scopes (only allow "read" and "write")
593+
allowedScopes := map[string]bool{"read": true, "write": true}
594+
validScopes := []string{}
595+
for _, s := range req.Scopes {
596+
if allowedScopes[s] {
597+
validScopes = append(validScopes, s)
598+
}
599+
}
600+
if len(validScopes) == 0 {
601+
validScopes = []string{"read", "write"}
602+
}
603+
scopesJSON, _ := json.Marshal(validScopes)
604+
589605
client := models.OAuthClient{
590606
ID: pkg.NewUUID(),
591607
ClientID: clientID,
592608
ClientSecretHash: string(secretHash),
593609
Name: req.Name,
594610
RedirectURIs: redirectURIsJSON,
595-
Scopes: `["read","write"]`,
611+
Scopes: string(scopesJSON),
596612
UserID: userID,
597613
CreatedAt: time.Now(),
598614
}

frontend/src/views/MCPConnections.vue

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<tr>
1414
<th>{{ $t('name') }}</th>
1515
<th>{{ $t('client_id') }}</th>
16+
<th>Redirect URIs</th>
1617
<th>{{ $t('scopes') }}</th>
1718
<th>Tạo lúc</th>
1819
<th>{{ $t('actions') }}</th>
@@ -23,7 +24,13 @@
2324
<td class="font-weight-medium">{{ client.name }}</td>
2425
<td class="text-body-2 font-mono">{{ client.client_id }}</td>
2526
<td>
26-
<v-chip size="x-small" variant="tonal" color="primary">{{ client.scopes }}</v-chip>
27+
<template v-if="parseJSON(client.redirect_uris).length">
28+
<v-chip v-for="uri in parseJSON(client.redirect_uris)" :key="uri" size="x-small" variant="tonal" class="mr-1 mb-1">{{ uri }}</v-chip>
29+
</template>
30+
<span v-else class="text-grey text-caption">Chưa cấu hình</span>
31+
</td>
32+
<td>
33+
<v-chip v-for="scope in parseJSON(client.scopes)" :key="scope" size="x-small" variant="tonal" color="primary" class="mr-1">{{ scope }}</v-chip>
2734
</td>
2835
<td class="text-caption">{{ new Date(client.created_at).toLocaleString('vi-VN') }}</td>
2936
<td>
@@ -42,12 +49,35 @@
4249
</div>
4350

4451
<!-- Create Dialog -->
45-
<v-dialog v-model="createDialog" max-width="520">
52+
<v-dialog v-model="createDialog" max-width="560">
4653
<v-card class="pa-6">
4754
<v-card-title>{{ $t('create_connection') }}</v-card-title>
48-
<v-text-field v-model="newName" :label="$t('name')" class="mt-4 mb-3" hint="Tên hiển thị cho kết nối này" persistent-hint />
4955

50-
<div v-if="generatedSecret" class="bg-grey-lighten-4 pa-4 rounded mb-3">
56+
<v-text-field v-model="newName" :label="$t('name')" class="mt-4" hint="Tên hiển thị cho kết nối này" persistent-hint />
57+
58+
<v-combobox
59+
v-model="newRedirectURIs"
60+
label="Redirect URIs"
61+
multiple
62+
chips
63+
closable-chips
64+
class="mt-4"
65+
hint="Nhập URL callback rồi nhấn Enter (vd: https://claude.ai/oauth/callback)"
66+
persistent-hint
67+
/>
68+
69+
<v-select
70+
v-model="newScopes"
71+
:items="scopeOptions"
72+
label="Phân quyền (Scopes)"
73+
multiple
74+
chips
75+
class="mt-4"
76+
hint="Chọn quyền truy cập cho kết nối"
77+
persistent-hint
78+
/>
79+
80+
<div v-if="generatedSecret" class="bg-grey-lighten-4 pa-4 rounded mt-4">
5181
<div class="text-caption text-grey mb-1">{{ $t('client_id') }}</div>
5282
<div class="font-mono text-body-2 mb-3">{{ generatedClientId }}</div>
5383
<div class="text-caption text-grey mb-1">{{ $t('client_secret') }}</div>
@@ -61,7 +91,7 @@
6191
</v-btn>
6292
</div>
6393

64-
<v-card-actions class="px-0">
94+
<v-card-actions class="px-0 mt-4">
6595
<v-spacer />
6696
<v-btn variant="text" @click="closeDialog">{{ generatedSecret ? 'Đóng' : $t('cancel') }}</v-btn>
6797
<v-btn v-if="!generatedSecret" color="primary" :loading="creating" :disabled="!newName" @click="generateClient">{{ $t('create') }}</v-btn>
@@ -80,6 +110,9 @@ import api from '../api'
80110
const clients = ref<any[]>([])
81111
const createDialog = ref(false)
82112
const newName = ref('')
113+
const newRedirectURIs = ref<string[]>([])
114+
const newScopes = ref<string[]>(['read', 'write'])
115+
const scopeOptions = ['read', 'write']
83116
const generatedClientId = ref('')
84117
const generatedSecret = ref('')
85118
const creating = ref(false)
@@ -88,6 +121,14 @@ const snackText = ref('')
88121
89122
onMounted(loadClients)
90123
124+
function parseJSON(val: string): string[] {
125+
try {
126+
return JSON.parse(val) || []
127+
} catch {
128+
return []
129+
}
130+
}
131+
91132
async function loadClients() {
92133
try {
93134
const { data } = await api.get('/mcp/clients')
@@ -98,11 +139,17 @@ async function loadClients() {
98139
async function generateClient() {
99140
creating.value = true
100141
try {
101-
const { data } = await api.post('/mcp/clients', { name: newName.value })
142+
const { data } = await api.post('/mcp/clients', {
143+
name: newName.value,
144+
redirect_uris: newRedirectURIs.value,
145+
scopes: newScopes.value,
146+
})
102147
generatedClientId.value = data.client_id
103148
generatedSecret.value = data.client_secret
104149
await loadClients()
105150
newName.value = ''
151+
newRedirectURIs.value = []
152+
newScopes.value = ['read', 'write']
106153
} catch (err: any) {
107154
snackText.value = err.response?.data?.error || 'Lỗi tạo kết nối'
108155
snackbar.value = true

0 commit comments

Comments
 (0)