Skip to content

Commit 680ae35

Browse files
author
Yuan Huang
committed
feat: add project scope selector and inline scope editing for MCP tokens
- createMcpToken form: native checkbox list for project selection - token list: edit button expands inline scope editor per token - PATCH /api/v1/mcp/tokens/<id> via new updateMcpToken service method - NewAPIService: add patch() method - validation: show error messages on invalid name/days input
1 parent 8886e52 commit 680ae35

3 files changed

Lines changed: 150 additions & 30 deletions

File tree

src/dashboard-client/src/components/user/UserGlobalTokens.vue

Lines changed: 132 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -207,19 +207,33 @@
207207
<md-card-area>
208208
<md-list class="m-t-md m-b-md">
209209
<md-list-item>
210-
<md-input-container class="m-r-sm" style="flex: 2">
210+
<md-input-container class="m-r-sm" style="flex: 2" :class="{'md-input-invalid': mcpNameError}">
211211
<label>Token Name (e.g. "Claude Desktop")</label>
212212
<md-input v-model="mcpForm.name" @keyup.enter.native="createMcpToken"></md-input>
213+
<span class="md-error" v-if="mcpNameError">Name must be at least 3 characters</span>
213214
</md-input-container>
214-
<md-input-container class="m-r-sm" style="flex: 0 0 160px">
215+
<md-input-container class="m-r-sm" style="flex: 0 0 160px" :class="{'md-input-invalid': mcpDaysError}">
215216
<label>Validity (days)</label>
216217
<md-input v-model.number="mcpForm.expiresDays" type="number" min="1" max="365" placeholder="365"></md-input>
218+
<span class="md-error" v-if="mcpDaysError">1–365 days</span>
217219
</md-input-container>
218-
<md-button :disabled="disableMcpAdd" class="md-icon-button md-list-action" @click="createMcpToken">
220+
<md-button class="md-icon-button md-list-action" @click="createMcpToken">
219221
<md-icon md-theme="running" class="md-primary">add_circle</md-icon>
220222
<md-tooltip>Create MCP token</md-tooltip>
221223
</md-button>
222224
</md-list-item>
225+
<md-list-item v-if="userProjects.length > 0" style="flex-wrap: wrap; align-items: flex-start; padding: 0 16px 8px">
226+
<div style="width: 100%; font-size: 13px; color: #666; margin-bottom: 6px;">
227+
Project scope
228+
<small style="color: #999; margin-left: 6px;">leave all unchecked to allow access to all projects</small>
229+
</div>
230+
<div style="display: flex; flex-wrap: wrap; gap: 4px 16px;">
231+
<label v-for="p in userProjects" :key="p.id" class="mcp-project-checkbox">
232+
<input type="checkbox" :value="p.id" v-model="mcpForm.selectedProjects">
233+
{{ p.name }}
234+
</label>
235+
</div>
236+
</md-list-item>
223237
</md-list>
224238
</md-card-area>
225239
</md-card>
@@ -239,30 +253,56 @@
239253
</md-table-row>
240254
</md-table-header>
241255
<md-table-body>
242-
<md-table-row v-for="t in mcpTokens" :key="t.token_id">
243-
<md-table-cell>{{ t.name }}</md-table-cell>
244-
<md-table-cell>
245-
<span v-if="!t.enabled_projects || Object.keys(t.enabled_projects).length === 0" class="mcp-all-projects">all projects</span>
246-
<span v-else class="mcp-project-count">{{ Object.keys(t.enabled_projects).length }} project(s)</span>
247-
</md-table-cell>
248-
<md-table-cell>{{ formatDate(t.created_at) }}</md-table-cell>
249-
<md-table-cell>
250-
<span :class="expiryClass(t.expires_at)">
251-
{{ formatDate(t.expires_at) }}
252-
<md-icon v-if="isExpiringSoon(t.expires_at)" style="font-size:16px;vertical-align:middle">warning</md-icon>
253-
</span>
254-
</md-table-cell>
255-
<md-table-cell>{{ t.last_used_at ? formatDate(t.last_used_at) : '—' }}</md-table-cell>
256-
<md-table-cell>
257-
<md-switch v-model="t.allow_trigger" @change="toggleMcpTrigger(t)" class="mcp-trigger-switch"></md-switch>
258-
</md-table-cell>
259-
<md-table-cell>
260-
<md-button class="md-icon-button" @click="confirmMcpRevoke(t)">
261-
<md-icon class="md-primary">delete</md-icon>
262-
<md-tooltip>Revoke token</md-tooltip>
263-
</md-button>
264-
</md-table-cell>
265-
</md-table-row>
256+
<template v-for="t in mcpTokens">
257+
<md-table-row :key="t.token_id">
258+
<md-table-cell>{{ t.name }}</md-table-cell>
259+
<md-table-cell>
260+
<span v-if="!t.enabled_projects || Object.keys(t.enabled_projects).length === 0" class="mcp-all-projects">all projects</span>
261+
<span v-else class="mcp-project-count">{{ Object.keys(t.enabled_projects).length }} project(s)</span>
262+
</md-table-cell>
263+
<md-table-cell>{{ formatDate(t.created_at) }}</md-table-cell>
264+
<md-table-cell>
265+
<span :class="expiryClass(t.expires_at)">
266+
{{ formatDate(t.expires_at) }}
267+
<md-icon v-if="isExpiringSoon(t.expires_at)" style="font-size:16px;vertical-align:middle">warning</md-icon>
268+
</span>
269+
</md-table-cell>
270+
<md-table-cell>{{ t.last_used_at ? formatDate(t.last_used_at) : '—' }}</md-table-cell>
271+
<md-table-cell>
272+
<md-switch v-model="t.allow_trigger" @change="toggleMcpTrigger(t)" class="mcp-trigger-switch"></md-switch>
273+
</md-table-cell>
274+
<md-table-cell>
275+
<md-button class="md-icon-button" @click="toggleScopeEdit(t)">
276+
<md-icon>edit</md-icon>
277+
<md-tooltip>Edit project scope</md-tooltip>
278+
</md-button>
279+
<md-button class="md-icon-button" @click="confirmMcpRevoke(t)">
280+
<md-icon class="md-primary">delete</md-icon>
281+
<md-tooltip>Revoke token</md-tooltip>
282+
</md-button>
283+
</md-table-cell>
284+
</md-table-row>
285+
286+
<!-- Inline scope editor -->
287+
<md-table-row v-if="scopeEditId === t.token_id" :key="t.token_id + '-scope'" class="log-row">
288+
<md-table-cell colspan="7" class="log-cell">
289+
<div style="padding: 8px 0;">
290+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">
291+
Project scope
292+
<small style="color: #999; margin-left: 6px;">leave all unchecked to allow access to all projects</small>
293+
</div>
294+
<div style="display: flex; flex-wrap: wrap; gap: 4px 16px; margin-bottom: 12px;">
295+
<label v-for="p in userProjects" :key="p.id" class="mcp-project-checkbox">
296+
<input type="checkbox" :value="p.id" v-model="scopeEditSelection">
297+
{{ p.name }}
298+
</label>
299+
</div>
300+
<md-button class="md-raised md-primary md-dense" @click="saveScopeEdit(t)">Save</md-button>
301+
<md-button class="md-dense" @click="scopeEditId = null">Cancel</md-button>
302+
</div>
303+
</md-table-cell>
304+
</md-table-row>
305+
</template>
266306

267307
<md-table-row v-if="mcpTokens.length === 0">
268308
<md-table-cell colspan="7">No MCP tokens yet. Create one above.</md-table-cell>
@@ -321,9 +361,12 @@ export default {
321361
mcpTokens: [],
322362
newMcpToken: '',
323363
pendingMcpRevoke: null,
364+
scopeEditId: null,
365+
scopeEditSelection: [],
324366
mcpForm: {
325367
name: '',
326-
expiresDays: 365
368+
expiresDays: 365,
369+
selectedProjects: []
327370
}
328371
}),
329372
@@ -336,6 +379,16 @@ export default {
336379
return !this.mcpForm.name || this.mcpForm.name.length < 3 ||
337380
!this.mcpForm.expiresDays || this.mcpForm.expiresDays < 1 || this.mcpForm.expiresDays > 365
338381
},
382+
mcpNameError () {
383+
return this.mcpForm.name !== '' && this.mcpForm.name.length < 3
384+
},
385+
mcpDaysError () {
386+
return this.mcpForm.expiresDays !== '' && this.mcpForm.expiresDays !== null &&
387+
(this.mcpForm.expiresDays < 1 || this.mcpForm.expiresDays > 365)
388+
},
389+
userProjects () {
390+
return this.$store.state.projects || []
391+
},
339392
adminProjects () {
340393
return this.$store.state.projects.filter(p => p.userHasAdminRights())
341394
},
@@ -437,8 +490,19 @@ export default {
437490
},
438491
439492
createMcpToken () {
440-
if (this.disableMcpAdd) return
441-
UserTokenService.createMcpToken(this.mcpForm.name, {}, this.mcpForm.expiresDays)
493+
if (!this.mcpForm.name || this.mcpForm.name.length < 3) {
494+
NotificationService.$emit('NOTIFICATION', new Notification({ message: 'Token name must be at least 3 characters.' }))
495+
return
496+
}
497+
const days = this.mcpForm.expiresDays
498+
if (!days || days < 1 || days > 365) {
499+
NotificationService.$emit('NOTIFICATION', new Notification({ message: 'Validity must be between 1 and 365 days.' }))
500+
return
501+
}
502+
const enabledProjects = this.mcpForm.selectedProjects.length > 0
503+
? this.mcpForm.selectedProjects.reduce((acc, id) => { acc[id] = null; return acc }, {})
504+
: {}
505+
UserTokenService.createMcpToken(this.mcpForm.name, enabledProjects, this.mcpForm.expiresDays)
442506
.then((result) => {
443507
this.mcpTokens.unshift({
444508
token_id: result.token_id,
@@ -453,6 +517,7 @@ export default {
453517
this.$refs['mcpTokenDialog'].open()
454518
this.mcpForm.name = ''
455519
this.mcpForm.expiresDays = 365
520+
this.mcpForm.selectedProjects = []
456521
})
457522
.catch(() => {})
458523
},
@@ -477,6 +542,29 @@ export default {
477542
.finally(() => { this.pendingMcpRevoke = null })
478543
},
479544
545+
toggleScopeEdit (token) {
546+
if (this.scopeEditId === token.token_id) {
547+
this.scopeEditId = null
548+
return
549+
}
550+
this.scopeEditId = token.token_id
551+
this.scopeEditSelection = Object.keys(token.enabled_projects || {})
552+
},
553+
554+
saveScopeEdit (token) {
555+
const enabledProjects = this.scopeEditSelection.reduce((acc, id) => {
556+
acc[id] = null
557+
return acc
558+
}, {})
559+
UserTokenService.updateMcpToken(token.token_id, enabledProjects)
560+
.then(() => {
561+
token.enabled_projects = enabledProjects
562+
this.scopeEditId = null
563+
NotificationService.$emit('NOTIFICATION', new Notification({ message: `Scope updated for "${token.name}".` }))
564+
})
565+
.catch(() => {})
566+
},
567+
480568
toggleMcpTrigger (token) {
481569
const newVal = !token.allow_trigger
482570
UserTokenService.setMcpTrigger(token.token_id, newVal)
@@ -576,4 +664,18 @@ export default {
576664
.mcp-trigger-switch {
577665
margin: 0;
578666
}
667+
668+
.mcp-project-checkbox {
669+
display: inline-flex;
670+
align-items: center;
671+
gap: 4px;
672+
font-size: 13px;
673+
cursor: pointer;
674+
user-select: none;
675+
color: #444;
676+
}
677+
678+
.mcp-project-checkbox input {
679+
cursor: pointer;
680+
}
579681
</style>

src/dashboard-client/src/services/NewAPIService.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ class NewAPIService {
6161
})
6262
.catch(this._handleError(false))
6363
}
64+
65+
patch (url, payload) {
66+
console.log(`PATCH API: ${url}`)
67+
const u = this.api + url
68+
return Vue.http.patch(u, payload)
69+
.then((response) => {
70+
return response.body
71+
})
72+
.catch(this._handleError(false))
73+
}
6474
}
6575

6676
export default new NewAPIService()

src/dashboard-client/src/services/UserTokenService.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ class UserTokenService {
5959
})
6060
}
6161

62+
updateMcpToken (tokenId, enabledProjects) {
63+
return NewAPIService.patch(`mcp/tokens/${tokenId}`, { enabled_projects: enabledProjects })
64+
.catch((err) => {
65+
NotificationService.$emit('NOTIFICATION', new Notification(err))
66+
throw err
67+
})
68+
}
69+
6270
revokeMcpToken (tokenId) {
6371
return NewAPIService.delete(`mcp/tokens/${tokenId}`)
6472
.catch((err) => {

0 commit comments

Comments
 (0)