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 >
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 >
0 commit comments