@adminforth/mcp 1.4.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,19 @@
1
1
  <template>
2
- <div class="af-mcp-authorize flex min-h-screen items-center justify-center bg-gray-100 p-4 dark:bg-gray-800">
3
- <div class="w-full max-w-lg rounded-lg bg-white p-6 shadow dark:bg-gray-700">
2
+ <div class="af-mcp-authorize flex min-h-screen items-center justify-center bg-lightHtml p-4 dark:bg-darkHtml">
3
+ <Spinner v-if="!framed && !authorization && !error" class="h-8 w-8" />
4
+ <Card v-else class="af-mcp-authorize-card w-full max-w-lg">
4
5
  <template v-if="framed">
5
- <h1 class="text-xl font-medium text-gray-900 dark:text-white">{{ $t('Cannot connect MCP client') }}</h1>
6
- <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
6
+ <h1 :class="TITLE_CLASSES">{{ $t('Cannot connect MCP client') }}</h1>
7
+ <p :class="TEXT_CLASSES">
7
8
  {{ $t('This page is embedded in another site. Open it in a separate browser tab to connect your MCP client.') }}
8
9
  </p>
9
10
  </template>
10
11
  <template v-else-if="authorization">
11
- <h1 class="text-xl font-medium text-gray-900 dark:text-white">
12
+ <h1 :class="TITLE_CLASSES">
12
13
  {{ $t('Connect {client}?', { client: authorization.clientName }) }}
13
14
  </h1>
14
- <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
15
- <b>{{ authorization.clientName }}</b>
15
+ <p :class="TEXT_CLASSES">
16
+ <b class="font-semibold text-lightCardTitle dark:text-darkCardTitle">{{ authorization.clientName }}</b>
16
17
  <span v-if="authorization.clientHost"> ({{ authorization.clientHost }})</span>
17
18
  <template v-if="readOnly">
18
19
  {{ $t('asks to act on your behalf in {brand}: it will see all data you can see, but not change it.', { brand: brandName }) }}
@@ -24,10 +25,14 @@
24
25
  <Checkbox v-model="readOnlyChosen" :disabled="authorization.serverReadOnly" class="mt-4">
25
26
  {{ $t('Read only: the agent can only read data') }}
26
27
  </Checkbox>
27
- <p v-if="authorization.loopbackRedirect" class="mt-3 text-sm font-medium text-amber-700 dark:text-amber-300">
28
- {{ $t('After you answer, your browser returns to an app running on this computer. Allow only if you have just started connecting {client} yourself.', { client: authorization.clientName }) }}
29
- </p>
30
- <p v-else class="mt-3 text-sm text-gray-600 dark:text-gray-300">
28
+ <div
29
+ v-if="authorization.loopbackRedirect"
30
+ class="mt-4 flex items-start gap-2 rounded-default border border-lightWarningBorder bg-lightWarningSoft p-3 text-sm text-lightWarning dark:border-darkWarningBorder dark:bg-darkWarningSoft dark:text-darkWarning"
31
+ >
32
+ <IconTriangleAlert class="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
33
+ <p>{{ $t('After you answer, your browser returns to an app running on this computer. Allow only if you have just started connecting {client} yourself.', { client: authorization.clientName }) }}</p>
34
+ </div>
35
+ <p v-else :class="TEXT_CLASSES">
31
36
  {{ $t('After you answer, your browser returns to {host}.', { host: authorization.redirectHost }) }}
32
37
  </p>
33
38
  <div class="mt-6 flex justify-end gap-2">
@@ -35,24 +40,26 @@
35
40
  <Button :loader="resolving" :disabled="resolving || !armed" @click="resolve(true)">{{ $t('Allow') }}</Button>
36
41
  </div>
37
42
  </template>
38
- <template v-else-if="error">
39
- <h1 class="text-xl font-medium text-gray-900 dark:text-white">{{ $t('Cannot connect MCP client') }}</h1>
40
- <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">{{ error }}</p>
43
+ <template v-else>
44
+ <h1 :class="TITLE_CLASSES">{{ $t('Cannot connect MCP client') }}</h1>
45
+ <p :class="TEXT_CLASSES">{{ error }}</p>
41
46
  </template>
42
- <Spinner v-else class="mx-auto h-8 w-8" />
43
- </div>
47
+ </Card>
44
48
  </div>
45
49
  </template>
46
50
 
47
51
  <script setup lang="ts">
48
52
  import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
49
53
  import { useRoute } from 'vue-router';
50
- import { Button, Checkbox, Spinner } from '@/afcl';
54
+ import { Button, Card, Checkbox, Spinner } from '@/afcl';
55
+ import { IconTriangleAlert } from '@iconify-prerendered/vue-lucide';
51
56
  import { useCoreStore } from '@/stores/core';
52
57
  import { callAdminForthApi } from '@/utils';
53
58
 
54
59
  // Long enough to swallow the second click of a double click, too short for a reader to notice.
55
60
  const ALLOW_DELAY_MS = 500;
61
+ const TITLE_CLASSES = 'text-lg font-semibold tracking-tight text-lightCardTitle dark:text-darkCardTitle';
62
+ const TEXT_CLASSES = 'mt-2 text-sm text-lightCardDescription dark:text-darkCardDescription';
56
63
 
57
64
  type Authorization = {
58
65
  clientName: string;
@@ -1,24 +1,28 @@
1
1
  <template>
2
- <div class="af-mcp-confirm flex min-h-screen items-center justify-center bg-gray-100 p-4 dark:bg-gray-800">
3
- <div v-if="resultMessage || error" class="w-full max-w-lg rounded-lg bg-white p-6 shadow dark:bg-gray-700">
2
+ <div class="af-mcp-confirm flex min-h-screen items-center justify-center bg-lightHtml p-4 dark:bg-darkHtml">
3
+ <Card v-if="resultMessage || error" class="af-mcp-confirm-card w-full max-w-lg">
4
4
  <template v-if="pending">
5
- <h1 class="text-xl font-medium text-gray-900 dark:text-white">{{ title }}</h1>
6
- <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
7
- <b>{{ pending.connectionName }}</b>
5
+ <h1 :class="TITLE_CLASSES">{{ title }}</h1>
6
+ <p :class="TEXT_CLASSES">
7
+ <b class="font-semibold text-lightCardTitle dark:text-darkCardTitle">{{ pending.connectionName }}</b>
8
8
  {{ $t('asks to run this action on your behalf. | asks to run these actions on your behalf.', pending.actions.length) }}
9
9
  </p>
10
- <p class="mt-4 text-sm font-medium" :class="failed ? 'text-red-700 dark:text-red-300' : 'text-green-700 dark:text-green-300'">
10
+ <p
11
+ class="af-mcp-confirm-result mt-4 flex items-start gap-2 text-sm font-medium"
12
+ :class="failed ? 'text-lightDanger dark:text-darkDanger' : 'text-lightSuccess dark:text-darkSuccess'"
13
+ >
14
+ <component :is="failed ? IconCircleAlert : IconCircleCheck" class="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
11
15
  {{ resultMessage }}
12
16
  </p>
13
- <ul v-if="failedErrors.length" class="mt-2 list-disc space-y-1 pl-5 text-sm text-red-700 dark:text-red-300">
17
+ <ul v-if="failedErrors.length" class="mt-2 list-disc space-y-1 pl-10 text-sm text-lightDanger dark:text-darkDanger">
14
18
  <li v-for="(failedError, i) in failedErrors" :key="i">{{ failedError }}</li>
15
19
  </ul>
16
20
  </template>
17
21
  <template v-else>
18
- <h1 class="text-xl font-medium text-gray-900 dark:text-white">{{ $t('Cannot run the action') }}</h1>
19
- <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">{{ error }}</p>
22
+ <h1 :class="TITLE_CLASSES">{{ $t('Cannot run the action') }}</h1>
23
+ <p :class="TEXT_CLASSES">{{ error }}</p>
20
24
  </template>
21
- </div>
25
+ </Card>
22
26
  <Spinner v-else-if="running" class="h-8 w-8" />
23
27
  </div>
24
28
  </template>
@@ -27,7 +31,8 @@
27
31
  import { computed, onMounted, ref } from 'vue';
28
32
  import { useRoute } from 'vue-router';
29
33
  import { useI18n } from 'vue-i18n';
30
- import { Spinner } from '@/afcl';
34
+ import { Card, Spinner } from '@/afcl';
35
+ import { IconCircleAlert, IconCircleCheck } from '@iconify-prerendered/vue-lucide';
31
36
  import { callAdminForthApi } from '@/utils';
32
37
 
33
38
  type PendingAction = {
@@ -41,6 +46,9 @@ type PendingConfirmation = {
41
46
  actions: PendingAction[];
42
47
  };
43
48
  type ActionResult = { id: string; output: { error?: string }; isError: boolean };
49
+
50
+ const TITLE_CLASSES = 'text-lg font-semibold tracking-tight text-lightCardTitle dark:text-darkCardTitle';
51
+ const TEXT_CLASSES = 'mt-2 text-sm text-lightCardDescription dark:text-darkCardDescription';
44
52
  const { t } = useI18n();
45
53
  const route = useRoute();
46
54
  const pending = ref<PendingConfirmation | null>(null);
@@ -1,18 +1,16 @@
1
1
  <template>
2
- <div class="af-mcp-settings flex flex-col justify-center mr-6 md:mr-12">
3
- <div class="flex flex-wrap items-start justify-between gap-4">
4
- <div>
5
- <h2 class="text-3xl font-semibold leading-none text-gray-800 dark:text-gray-50">
6
- {{ $t('MCP Settings') }}
7
- </h2>
8
- <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
9
- {{ $t('Connect an AI agent to {brand}. It acts on your behalf with your permissions.', { brand: brandName }) }}
10
- </p>
11
- </div>
12
- <Button @click="openConnectDialog">
13
- {{ $t('Connect agent') }}
14
- </Button>
15
- </div>
2
+ <div class="af-mcp-settings flex flex-col">
3
+ <PageHeader
4
+ :title="$t('MCP')"
5
+ :description="$t('Connect an AI agent to {brand}. It acts on your behalf with your permissions.', { brand: brandName })"
6
+ >
7
+ <template #actions>
8
+ <Button class="af-mcp-connect-button" @click="openConnectDialog">
9
+ <IconPlus class="w-4 h-4" aria-hidden="true" />
10
+ {{ $t('Connect agent') }}
11
+ </Button>
12
+ </template>
13
+ </PageHeader>
16
14
 
17
15
  <Table
18
16
  class="mt-6"
@@ -22,16 +20,16 @@
22
20
  :pageSize="10"
23
21
  >
24
22
  <template #cell:name="{ item }">
25
- <p class="font-medium text-gray-900 dark:text-white">
23
+ <p class="flex flex-wrap items-center gap-2 font-medium text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">
26
24
  {{ item.name }}
27
25
  <span
28
26
  v-if="serverReadOnly || item.readOnly"
29
- class="ml-2 rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300"
27
+ class="af-mcp-read-only-badge inline-flex h-5 items-center rounded-full bg-lightSecondaryDarken px-2 text-xs font-medium text-lightInputIconColor dark:bg-darkSecondaryLighten dark:text-darkInputIconColor"
30
28
  >
31
29
  {{ $t('Read only') }}
32
30
  </span>
33
31
  </p>
34
- <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
32
+ <p class="mt-0.5 text-xs text-lightInputIconColor dark:text-darkInputIconColor">
35
33
  {{ item.oauthClientId ? `${$t('OAuth')} · ${oauthClientHost(item.oauthClientId)}` : $t('Auth secret') }} · {{ formatDateTime(item.createdAt) }}
36
34
  </p>
37
35
  </template>
@@ -41,17 +39,20 @@
41
39
  <img v-if="describeAgent(item.lastUsedByAgent).icon" :src="describeAgent(item.lastUsedByAgent).icon" class="h-5 w-5" alt="" />
42
40
  <span>{{ describeAgent(item.lastUsedByAgent).label }}</span>
43
41
  </div>
44
- <span v-else class="text-gray-400">{{ $t('Not used yet') }}</span>
42
+ <span v-else class="text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">{{ $t('Not used yet') }}</span>
45
43
  </template>
46
44
 
47
45
  <template #cell:lastUsedAt="{ item }">
48
- {{ item.lastUsedAt ? formatDateTime(item.lastUsedAt) : $t('Never') }}
46
+ <template v-if="item.lastUsedAt">{{ formatDateTime(item.lastUsedAt) }}</template>
47
+ <span v-else class="text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">{{ $t('Never') }}</span>
49
48
  </template>
50
49
 
51
50
  <template #cell:actions="{ item }">
52
51
  <div class="flex justify-end">
53
52
  <Button
54
- variant="danger"
53
+ class="af-mcp-revoke-button"
54
+ variant="secondary"
55
+ size="sm"
55
56
  :loader="revokingId === item.id"
56
57
  :disabled="revokingId === item.id"
57
58
  @click="revoke(item)"
@@ -64,33 +65,35 @@
64
65
 
65
66
  <Dialog
66
67
  ref="dialogRef"
67
- class="w-full max-w-3xl"
68
+ class="af-mcp-connect-dialog w-full max-w-3xl"
68
69
  :header="$t('Connect an AI agent')"
69
70
  :buttons="dialogButtons"
70
71
  >
71
72
  <ButtonGroup v-model="activeClient">
72
73
  <template v-for="client in CLIENTS" :key="client" #[`button:${client}`]>
73
- <span class="px-4 py-2">{{ client }}</span>
74
+ <span class="px-3.5 py-1.5">{{ client }}</span>
74
75
  </template>
75
76
  </ButtonGroup>
76
77
 
77
78
  <template v-if="showsCreatedSecret">
78
- <p class="mt-4 text-sm text-amber-700 dark:text-amber-300">{{ $t('Copy it now. You will not be able to view it again.') }}</p>
79
- <p class="mt-3 rounded-default border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-200">
80
- <template v-if="createdSecretReadOnly">
79
+ <p class="mt-4 text-sm font-medium text-lightWarning dark:text-darkWarning">{{ $t('Copy it now. You will not be able to view it again.') }}</p>
80
+ <div class="mt-3 flex items-start gap-2 rounded-default border border-lightWarningBorder bg-lightWarningSoft p-3 text-sm text-lightWarning dark:border-darkWarningBorder dark:bg-darkWarningSoft dark:text-darkWarning">
81
+ <IconTriangleAlert class="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
82
+ <p v-if="createdSecretReadOnly">
81
83
  {{ $t('This secret lets an agent read all data you can see in the admin panel. Keep it like a password, give it to one agent only, and revoke it if that agent or its device is compromised.') }}
82
- </template>
83
- <template v-else>
84
+ </p>
85
+ <p v-else>
84
86
  {{ $t('This secret lets an agent do everything you can do in the admin panel, including deleting data and changing your security settings. Keep it like a password, give it to one agent only, and revoke it if that agent or its device is compromised.') }}
85
- </template>
86
- </p>
87
+ </p>
88
+ </div>
87
89
  </template>
88
90
 
89
- <div v-for="step in activeSteps" :key="step.snippet" class="mt-4">
90
- <p class="text-sm text-gray-500 dark:text-gray-400">{{ step.hint }}</p>
91
+ <div v-for="step in activeSteps" :key="step.snippet" class="af-mcp-setup-step mt-4">
92
+ <p class="text-sm text-lightInputIconColor dark:text-darkInputIconColor">{{ step.hint }}</p>
91
93
  <div class="mt-2 flex items-start gap-2">
92
- <code class="min-w-0 flex-1 overflow-x-auto whitespace-pre rounded-default bg-gray-100 px-3 py-2 text-sm text-gray-800 dark:bg-gray-900 dark:text-gray-100">{{ step.snippet }}</code>
94
+ <code :class="SNIPPET_CLASSES" class="min-w-0 flex-1 overflow-x-auto whitespace-pre">{{ step.snippet }}</code>
93
95
  <Button variant="secondary" @click="copy(step.snippet, $t('Copied'))">
96
+ <IconCopy class="w-4 h-4" aria-hidden="true" />
94
97
  {{ $t('Copy') }}
95
98
  </Button>
96
99
  </div>
@@ -98,8 +101,8 @@
98
101
 
99
102
  <!-- enter is handled on the wrapper so it fires once: Input spreads attrs on both its root and the input -->
100
103
  <div v-if="activeClient === 'Other' && !createdSecret" class="mt-4" @keydown.enter="createAuthSecret">
101
- <p class="text-sm text-gray-500 dark:text-gray-400">{{ $t('Use one auth secret for one agent.') }}</p>
102
- <label class="mt-5 block text-sm font-medium text-gray-700 dark:text-gray-200">{{ $t('Secret name') }}</label>
104
+ <p class="text-sm text-lightInputIconColor dark:text-darkInputIconColor">{{ $t('Use one auth secret for one agent.') }}</p>
105
+ <label class="mt-5 block text-sm font-medium text-lightTextLabel dark:text-darkTextLabel">{{ $t('Secret name') }}</label>
103
106
  <div class="mt-2">
104
107
  <Input v-model="secretName" type="text" fullWidth placeholder="Claude Code" />
105
108
  </div>
@@ -109,8 +112,8 @@
109
112
  </div>
110
113
 
111
114
  <template v-if="showsCreatedSecret">
112
- <p class="mt-5 text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Prompt for your agent') }}</p>
113
- <pre class="mt-2 whitespace-pre-wrap rounded-default bg-gray-100 p-3 text-sm text-gray-800 dark:bg-gray-900 dark:text-gray-100">{{ setupPrompt }}</pre>
115
+ <p class="mt-4 text-sm text-lightInputIconColor dark:text-darkInputIconColor">{{ $t('Prompt for your agent') }}</p>
116
+ <pre :class="SNIPPET_CLASSES" class="mt-2 whitespace-pre-wrap">{{ setupPrompt }}</pre>
114
117
  </template>
115
118
  </Dialog>
116
119
  </div>
@@ -119,7 +122,8 @@
119
122
  <script setup lang="ts">
120
123
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
121
124
  import { useI18n } from 'vue-i18n';
122
- import { Button, ButtonGroup, Checkbox, Dialog, Input, Table } from '@/afcl';
125
+ import { Button, ButtonGroup, Checkbox, Dialog, Input, PageHeader, Table } from '@/afcl';
126
+ import { IconCopy, IconPlus, IconTriangleAlert } from '@iconify-prerendered/vue-lucide';
123
127
  import { useCoreStore } from '@/stores/core';
124
128
  import adminforth from '@/adminforth';
125
129
  import { callAdminForthApi, formatDateTime } from '@/utils';
@@ -142,6 +146,7 @@ type Client = typeof CLIENTS[number];
142
146
  const CLIENTS = ['Claude Code', 'Codex', 'Other'] as const;
143
147
  const NON_SERVER_NAME_CHARACTER_RE = /[^a-z0-9]+/g;
144
148
  const EDGE_HYPHEN_RE = /^-+|-+$/g;
149
+ const SNIPPET_CLASSES = 'rounded-default bg-lightSecondaryDarken px-3 py-1.5 font-mono text-[13px] leading-[22px] text-lightFormFieldTextColor dark:bg-darkSecondaryLighten dark:text-darkFormFieldTextColor';
145
150
 
146
151
  const { t } = useI18n();
147
152
  const coreStore = useCoreStore();
package/dist/index.js CHANGED
@@ -78,8 +78,8 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
78
78
  auth.userMenuSettingsPages ??= [];
79
79
  if (!auth.userMenuSettingsPages.some((page) => page.slug === 'mcp')) {
80
80
  auth.userMenuSettingsPages.push({
81
- icon: 'flowbite:server-outline',
82
- pageLabel: 'MCP Settings',
81
+ icon: 'lucide:bot',
82
+ pageLabel: 'MCP',
83
83
  slug: 'mcp',
84
84
  component: this.componentPath('McpSettings.vue'),
85
85
  isVisible: () => true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/mcp",
3
- "version": "1.4.3",
3
+ "version": "2.0.0",
4
4
  "description": "Expose AdminForth APIs as an authenticated MCP server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "typescript": "^5.7.3"
50
50
  },
51
51
  "peerDependencies": {
52
- "adminforth": "^3.32.0",
52
+ "adminforth": "^4.1.0",
53
53
  "express": "^4.21.0 || ^5.0.0"
54
54
  },
55
55
  "release": {