From b7de9289ab087b6bff54dbba576b66d4311a7835 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Thu, 25 Jun 2026 16:14:57 +0800 Subject: [PATCH 1/9] feat: add MCP token management UI to My Tokens page - UserTokenService: add loadMcpTokens, createMcpToken, revokeMcpToken, setMcpTrigger methods calling /api/v1/mcp/tokens/* endpoints - UserGlobalTokens.vue: append MCP Tokens card to /user/tokens page with create form, token list (name, project scope, expiry, last used, trigger toggle), revoke action, and post-create dialog showing the ib_mcp_* token value with INFRABOX_MCP_TOKEN usage hint --- .../src/components/user/UserGlobalTokens.vue | 186 ++++++++++++++++++ .../src/services/UserTokenService.js | 37 ++++ 2 files changed, 223 insertions(+) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index e71e5716f..d6b5deb28 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -186,6 +186,115 @@ md-cancel-text="Cancel" @close="onRevokeClose"> + + + + + +

+ + MCP Tokens + + For use with the InfraBox MCP server (INFRABOX_MCP_TOKEN) + + +

+
+
+ + + + + + + + + + + + + + + + add_circle + Create MCP token + + + + + + + + + + + + Name + Projects + Created + Expires + Last Used + Trigger + Actions + + + + + {{ t.name }} + + all projects + {{ Object.keys(t.enabled_projects).length }} project(s) + + {{ formatDate(t.created_at) }} + + + {{ formatDate(t.expires_at) }} + warning + + + {{ t.last_used_at ? formatDate(t.last_used_at) : '—' }} + + + + + + delete + Revoke token + + + + + + No MCP tokens yet. Create one above. + + + + +
+ + + + MCP Token Created + + Save this token somewhere safe — it will not be shown again.

+
{{ newMcpToken }}

+ Use it with the InfraBox MCP server:
+
$ export INFRABOX_MCP_TOKEN=<TOKEN_VALUE>
+
+ + OK + +
+ + + + @@ -208,6 +317,13 @@ export default { form: { description: '', expiresDays: 365 + }, + mcpTokens: [], + newMcpToken: '', + pendingMcpRevoke: null, + mcpForm: { + name: '', + expiresDays: 365 } }), @@ -216,6 +332,10 @@ export default { return !this.form.description || this.form.description.length < 3 || !this.form.expiresDays || this.form.expiresDays < 1 || this.form.expiresDays > 3650 }, + disableMcpAdd () { + return !this.mcpForm.name || this.mcpForm.name.length < 3 || + !this.mcpForm.expiresDays || this.mcpForm.expiresDays < 1 || this.mcpForm.expiresDays > 365 + }, adminProjects () { return this.$store.state.projects.filter(p => p.userHasAdminRights()) }, @@ -229,6 +349,10 @@ export default { this.tokens = tokens }).catch(() => {}) + UserTokenService.loadMcpTokens().then((tokens) => { + this.mcpTokens = tokens + }).catch(() => {}) + const adminProjects = this.$store.state.projects.filter(p => p.userHasAdminRights()) if (adminProjects.length > 0) { this.projectTokensLoading = true @@ -310,6 +434,54 @@ export default { .then((log) => { this.accessLog = log }) .catch(() => {}) .finally(() => { this.logLoading = false }) + }, + + createMcpToken () { + if (this.disableMcpAdd) return + UserTokenService.createMcpToken(this.mcpForm.name, {}, this.mcpForm.expiresDays) + .then((result) => { + this.mcpTokens.unshift({ + token_id: result.token_id, + name: result.name, + enabled_projects: result.enabled_projects, + allow_trigger: result.allow_trigger, + expires_at: result.expires_at, + created_at: new Date().toISOString(), + last_used_at: null + }) + this.newMcpToken = result.token + this.$refs['mcpTokenDialog'].open() + this.mcpForm.name = '' + this.mcpForm.expiresDays = 365 + }) + .catch(() => {}) + }, + + confirmMcpRevoke (token) { + this.pendingMcpRevoke = token + this.$refs['mcpRevokeDialog'].open() + }, + + onMcpRevokeClose (type) { + if (type !== 'ok' || !this.pendingMcpRevoke) { + this.pendingMcpRevoke = null + return + } + const target = this.pendingMcpRevoke + UserTokenService.revokeMcpToken(target.token_id) + .then(() => { + this.mcpTokens = this.mcpTokens.filter(t => t.token_id !== target.token_id) + NotificationService.$emit('NOTIFICATION', new Notification({ message: `MCP token "${target.name}" revoked.` })) + }) + .catch(() => {}) + .finally(() => { this.pendingMcpRevoke = null }) + }, + + toggleMcpTrigger (token) { + const newVal = !token.allow_trigger + UserTokenService.setMcpTrigger(token.token_id, newVal) + .then(() => { token.allow_trigger = newVal }) + .catch(() => {}) } } } @@ -385,4 +557,18 @@ export default { color: #c62828; font-weight: 500; } + +.mcp-all-projects { + color: #888; + font-style: italic; + font-size: 13px; +} + +.mcp-project-count { + font-size: 13px; +} + +.mcp-trigger-switch { + margin: 0; +} diff --git a/src/dashboard-client/src/services/UserTokenService.js b/src/dashboard-client/src/services/UserTokenService.js index 0eb879f06..4d398e4dd 100644 --- a/src/dashboard-client/src/services/UserTokenService.js +++ b/src/dashboard-client/src/services/UserTokenService.js @@ -38,6 +38,43 @@ class UserTokenService { throw err }) } + // MCP token methods — /api/v1/mcp/tokens/* + + loadMcpTokens () { + return NewAPIService.get('mcp/tokens/') + .catch((err) => { + NotificationService.$emit('NOTIFICATION', new Notification(err)) + throw err + }) + } + + createMcpToken (name, enabledProjects, expiresDays) { + return NewAPIService.post('mcp/tokens/', { + name, + enabled_projects: enabledProjects || {}, + expires_days: expiresDays || 365 + }).catch((err) => { + NotificationService.$emit('NOTIFICATION', new Notification(err)) + throw err + }) + } + + revokeMcpToken (tokenId) { + return NewAPIService.delete(`mcp/tokens/${tokenId}`) + .catch((err) => { + NotificationService.$emit('NOTIFICATION', new Notification(err)) + throw err + }) + } + + setMcpTrigger (tokenId, allow) { + const method = allow ? 'post' : 'delete' + return NewAPIService[method](`mcp/tokens/${tokenId}/trigger`, {}) + .catch((err) => { + NotificationService.$emit('NOTIFICATION', new Notification(err)) + throw err + }) + } } export default new UserTokenService() From 24d2d1a20a673ab2f025f52638b81b9248e5ee9b Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Thu, 25 Jun 2026 17:06:13 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?revert=20trigger=20switch=20on=20failure,=20clear=20token=20on?= =?UTF-8?q?=20dialog=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/user/UserGlobalTokens.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index d6b5deb28..c97fe33c2 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -282,7 +282,7 @@
$ export INFRABOX_MCP_TOKEN=<TOKEN_VALUE>
- OK + OK @@ -481,7 +481,12 @@ export default { const newVal = !token.allow_trigger UserTokenService.setMcpTrigger(token.token_id, newVal) .then(() => { token.allow_trigger = newVal }) - .catch(() => {}) + .catch(() => { token.allow_trigger = !newVal }) + }, + + closeMcpTokenDialog () { + this.$refs['mcpTokenDialog'].close() + this.newMcpToken = '' } } } From 34bdd8f6e6fbe10d80a8b6f8f500e3bfba6b3720 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Thu, 25 Jun 2026 17:20:33 +0800 Subject: [PATCH 3/9] fix: use v-model on md-switch instead of :value to fix render crash --- src/dashboard-client/src/components/user/UserGlobalTokens.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index c97fe33c2..0d5e1baca 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -254,7 +254,7 @@ {{ t.last_used_at ? formatDate(t.last_used_at) : '—' }} - + From 94d4e0f2eb20915b64dbccc252825dccd7ef8913 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Fri, 26 Jun 2026 15:03:34 +0800 Subject: [PATCH 4/9] 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/ via new updateMcpToken service method - NewAPIService: add patch() method - validation: show error messages on invalid name/days input --- .../src/components/user/UserGlobalTokens.vue | 162 ++++++++++++++---- .../src/services/NewAPIService.js | 10 ++ .../src/services/UserTokenService.js | 8 + 3 files changed, 150 insertions(+), 30 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index 0d5e1baca..efda6d229 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -207,19 +207,33 @@ - + + Name must be at least 3 characters - + + 1–365 days - + add_circle Create MCP token + +
+ Project scope + leave all unchecked to allow access to all projects +
+
+ +
+
@@ -239,30 +253,56 @@ - - {{ t.name }} - - all projects - {{ Object.keys(t.enabled_projects).length }} project(s) - - {{ formatDate(t.created_at) }} - - - {{ formatDate(t.expires_at) }} - warning - - - {{ t.last_used_at ? formatDate(t.last_used_at) : '—' }} - - - - - - delete - Revoke token - - - + No MCP tokens yet. Create one above. @@ -321,9 +361,12 @@ export default { mcpTokens: [], newMcpToken: '', pendingMcpRevoke: null, + scopeEditId: null, + scopeEditSelection: [], mcpForm: { name: '', - expiresDays: 365 + expiresDays: 365, + selectedProjects: [] } }), @@ -336,6 +379,16 @@ export default { return !this.mcpForm.name || this.mcpForm.name.length < 3 || !this.mcpForm.expiresDays || this.mcpForm.expiresDays < 1 || this.mcpForm.expiresDays > 365 }, + mcpNameError () { + return this.mcpForm.name !== '' && this.mcpForm.name.length < 3 + }, + mcpDaysError () { + return this.mcpForm.expiresDays !== '' && this.mcpForm.expiresDays !== null && + (this.mcpForm.expiresDays < 1 || this.mcpForm.expiresDays > 365) + }, + userProjects () { + return this.$store.state.projects || [] + }, adminProjects () { return this.$store.state.projects.filter(p => p.userHasAdminRights()) }, @@ -437,8 +490,19 @@ export default { }, createMcpToken () { - if (this.disableMcpAdd) return - UserTokenService.createMcpToken(this.mcpForm.name, {}, this.mcpForm.expiresDays) + if (!this.mcpForm.name || this.mcpForm.name.length < 3) { + NotificationService.$emit('NOTIFICATION', new Notification({ message: 'Token name must be at least 3 characters.' })) + return + } + const days = this.mcpForm.expiresDays + if (!days || days < 1 || days > 365) { + NotificationService.$emit('NOTIFICATION', new Notification({ message: 'Validity must be between 1 and 365 days.' })) + return + } + const enabledProjects = this.mcpForm.selectedProjects.length > 0 + ? this.mcpForm.selectedProjects.reduce((acc, id) => { acc[id] = null; return acc }, {}) + : {} + UserTokenService.createMcpToken(this.mcpForm.name, enabledProjects, this.mcpForm.expiresDays) .then((result) => { this.mcpTokens.unshift({ token_id: result.token_id, @@ -453,6 +517,7 @@ export default { this.$refs['mcpTokenDialog'].open() this.mcpForm.name = '' this.mcpForm.expiresDays = 365 + this.mcpForm.selectedProjects = [] }) .catch(() => {}) }, @@ -477,6 +542,29 @@ export default { .finally(() => { this.pendingMcpRevoke = null }) }, + toggleScopeEdit (token) { + if (this.scopeEditId === token.token_id) { + this.scopeEditId = null + return + } + this.scopeEditId = token.token_id + this.scopeEditSelection = Object.keys(token.enabled_projects || {}) + }, + + saveScopeEdit (token) { + const enabledProjects = this.scopeEditSelection.reduce((acc, id) => { + acc[id] = null + return acc + }, {}) + UserTokenService.updateMcpToken(token.token_id, enabledProjects) + .then(() => { + token.enabled_projects = enabledProjects + this.scopeEditId = null + NotificationService.$emit('NOTIFICATION', new Notification({ message: `Scope updated for "${token.name}".` })) + }) + .catch(() => {}) + }, + toggleMcpTrigger (token) { const newVal = !token.allow_trigger UserTokenService.setMcpTrigger(token.token_id, newVal) @@ -576,4 +664,18 @@ export default { .mcp-trigger-switch { margin: 0; } + +.mcp-project-checkbox { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 13px; + cursor: pointer; + user-select: none; + color: #444; +} + +.mcp-project-checkbox input { + cursor: pointer; +} diff --git a/src/dashboard-client/src/services/NewAPIService.js b/src/dashboard-client/src/services/NewAPIService.js index 132d39160..23f654f42 100644 --- a/src/dashboard-client/src/services/NewAPIService.js +++ b/src/dashboard-client/src/services/NewAPIService.js @@ -61,6 +61,16 @@ class NewAPIService { }) .catch(this._handleError(false)) } + + patch (url, payload) { + console.log(`PATCH API: ${url}`) + const u = this.api + url + return Vue.http.patch(u, payload) + .then((response) => { + return response.body + }) + .catch(this._handleError(false)) + } } export default new NewAPIService() diff --git a/src/dashboard-client/src/services/UserTokenService.js b/src/dashboard-client/src/services/UserTokenService.js index 4d398e4dd..c694a6dd8 100644 --- a/src/dashboard-client/src/services/UserTokenService.js +++ b/src/dashboard-client/src/services/UserTokenService.js @@ -59,6 +59,14 @@ class UserTokenService { }) } + updateMcpToken (tokenId, enabledProjects) { + return NewAPIService.patch(`mcp/tokens/${tokenId}`, { enabled_projects: enabledProjects }) + .catch((err) => { + NotificationService.$emit('NOTIFICATION', new Notification(err)) + throw err + }) + } + revokeMcpToken (tokenId) { return NewAPIService.delete(`mcp/tokens/${tokenId}`) .catch((err) => { From b025931bd36366601fc213e0e19d4c60e32e3ab1 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Fri, 26 Jun 2026 16:59:41 +0800 Subject: [PATCH 5/9] fix: empty enabled_projects should allow all projects, not deny all - projects.py: empty dict falls through to full collaborator query - auth.py: check_project_access_mcp treats empty dict as allow-all --- src/api/handlers/mcp/auth.py | 4 ++++ src/api/handlers/mcp/routes/projects.py | 31 ++++++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/api/handlers/mcp/auth.py b/src/api/handlers/mcp/auth.py index c72e51d5b..958cc97bd 100644 --- a/src/api/handlers/mcp/auth.py +++ b/src/api/handlers/mcp/auth.py @@ -116,6 +116,10 @@ def check_project_access_mcp(project_id: str) -> bool: return True enabled = g.mcp_enabled_projects + if not enabled: + # empty dict = all projects allowed + return True + if project_id not in enabled: return False diff --git a/src/api/handlers/mcp/routes/projects.py b/src/api/handlers/mcp/routes/projects.py index e82fcd9b5..6446c1631 100644 --- a/src/api/handlers/mcp/routes/projects.py +++ b/src/api/handlers/mcp/routes/projects.py @@ -27,21 +27,24 @@ def get(self): enabled = getattr(g, 'mcp_enabled_projects', None) if enabled is not None: - # MCP token path: return only scoped projects where the token owner - # is still a collaborator — re-validate membership at query time so - # a revoked collaborator cannot enumerate project metadata. + # MCP token path if not enabled: - audit_mcp('list_projects', outcome='success', details={'count': 0}) - return [] - - project_ids = list(enabled.keys()) - rows = g.db.execute_many_dict(''' - SELECT p.id, p.name, p.type, p.public - FROM project p - INNER JOIN collaborator co ON co.project_id = p.id AND co.user_id = %s - WHERE p.id = ANY(%s::uuid[]) - ORDER BY p.name - ''', [user_id, project_ids]) + # empty dict = all projects the user is a collaborator on + rows = g.db.execute_many_dict(''' + SELECT p.id, p.name, p.type, p.public + FROM project p + INNER JOIN collaborator co ON co.project_id = p.id AND co.user_id = %s + ORDER BY p.name + ''', [user_id]) + else: + project_ids = list(enabled.keys()) + rows = g.db.execute_many_dict(''' + SELECT p.id, p.name, p.type, p.public + FROM project p + INNER JOIN collaborator co ON co.project_id = p.id AND co.user_id = %s + WHERE p.id = ANY(%s::uuid[]) + ORDER BY p.name + ''', [user_id, project_ids]) else: # Session path: return all projects the user is a collaborator on rows = g.db.execute_many_dict(''' From f871b43536ef18abac061f6d325717ebba6ddca3 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Fri, 26 Jun 2026 17:43:00 +0800 Subject: [PATCH 6/9] chore: force api layer cache invalidation for mcp projects fix --- src/api/handlers/mcp/routes/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/handlers/mcp/routes/projects.py b/src/api/handlers/mcp/routes/projects.py index 6446c1631..4df144aad 100644 --- a/src/api/handlers/mcp/routes/projects.py +++ b/src/api/handlers/mcp/routes/projects.py @@ -61,3 +61,4 @@ def get(self): except Exception as exc: audit_mcp('list_projects', outcome='failure', error=str(exc)) raise + From 3de3cad3c6d3f31d3b41cc9ad9535ebfc3bfea67 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Mon, 29 Jun 2026 10:57:06 +0800 Subject: [PATCH 7/9] fix: fix scope editor button overflow and project checkbox alignment --- .../src/components/user/UserGlobalTokens.vue | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index efda6d229..bdc2e215f 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -222,8 +222,8 @@ Create MCP token
- -
+
+
Project scope leave all unchecked to allow access to all projects
@@ -233,7 +233,7 @@ {{ p.name }}
- +
@@ -297,8 +297,8 @@ {{ p.name }} - Save - Cancel + Save + Cancel
@@ -603,6 +603,7 @@ export default { .log-cell { background-color: #f9f9f9 !important; padding: 8px 16px !important; + overflow: visible !important; } .log-table { From 151248729bd1fda20d60434e5d63bfd68f2d396b Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Mon, 29 Jun 2026 11:05:07 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?empty=20scope=20semantics,=20trigger=20toggle,=20patch=20respon?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.py: clarify enabled_projects={} means allow-all (not deny-all); add docstring explaining the intentional semantics - UserGlobalTokens.vue: fix toggleMcpTrigger — v-model already flips token.allow_trigger before @change fires, so read it directly instead of negating again (previous code sent the wrong value and silently reverted the UI) - NewAPIService.js: patch() returns response.body || {} to guard against 204 No Content responses --- src/api/handlers/mcp/auth.py | 6 +++++- .../src/components/user/UserGlobalTokens.vue | 5 +++-- src/dashboard-client/src/services/NewAPIService.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/api/handlers/mcp/auth.py b/src/api/handlers/mcp/auth.py index 958cc97bd..fdfd7688f 100644 --- a/src/api/handlers/mcp/auth.py +++ b/src/api/handlers/mcp/auth.py @@ -107,6 +107,10 @@ def decorated(*args, **kwargs): def check_project_access_mcp(project_id: str) -> bool: """Return True if the current request may access project_id. + enabled_projects semantics (intentional): + {} → allow all projects (user chose no restriction when creating the token) + {id: ...} → allow only the listed project IDs + MCP token path: project must be in g.mcp_enabled_projects and not past its per-project expiry (if set). Session path: delegates to OPA (already checked in before_request). @@ -117,7 +121,7 @@ def check_project_access_mcp(project_id: str) -> bool: enabled = g.mcp_enabled_projects if not enabled: - # empty dict = all projects allowed + # empty dict → no project restriction, allow all return True if project_id not in enabled: diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index bdc2e215f..febd67d4c 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -566,9 +566,10 @@ export default { }, toggleMcpTrigger (token) { - const newVal = !token.allow_trigger + // v-model has already flipped token.allow_trigger to the new value; + // read it directly rather than negating again. + const newVal = token.allow_trigger UserTokenService.setMcpTrigger(token.token_id, newVal) - .then(() => { token.allow_trigger = newVal }) .catch(() => { token.allow_trigger = !newVal }) }, diff --git a/src/dashboard-client/src/services/NewAPIService.js b/src/dashboard-client/src/services/NewAPIService.js index 23f654f42..2989427c0 100644 --- a/src/dashboard-client/src/services/NewAPIService.js +++ b/src/dashboard-client/src/services/NewAPIService.js @@ -67,7 +67,7 @@ class NewAPIService { const u = this.api + url return Vue.http.patch(u, payload) .then((response) => { - return response.body + return response.body || {} }) .catch(this._handleError(false)) } From 8f86936d9ad02f47eeca9361e65dc9ae04f9a6a8 Mon Sep 17 00:00:00 2001 From: Yuan Huang Date: Mon, 29 Jun 2026 15:48:07 +0800 Subject: [PATCH 9/9] fix: replace md-button with native button in scope editor to fix overflow clipping --- .../src/components/user/UserGlobalTokens.vue | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index febd67d4c..20f92a254 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -297,8 +297,8 @@ {{ p.name }} - Save - Cancel + + @@ -680,4 +680,32 @@ export default { .mcp-project-checkbox input { cursor: pointer; } + +.scope-btn { + display: inline-block; + margin-top: 8px; + margin-right: 8px; + padding: 6px 16px; + border: none; + border-radius: 2px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + text-transform: uppercase; + background: #e0e0e0; + color: #212121; +} + +.scope-btn:hover { + background: rgba(0,0,0,0.07); +} + +.scope-btn-primary { + background: #009688; + color: #fff; +} + +.scope-btn-primary:hover { + background: #00796b; +}