@adminforth/mcp 1.4.2 → 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.
- package/dist/adminPanelConfirmation.js +2 -1
- package/dist/apiTools.js +2 -1
- package/dist/custom/McpAuthorize.vue +24 -17
- package/dist/custom/McpConfirm.vue +19 -11
- package/dist/custom/McpSettings.vue +42 -37
- package/dist/index.js +2 -2
- package/dist/toolOutput.d.ts +1 -0
- package/dist/toolOutput.js +13 -0
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { MAX_ACTIONS_PER_CONFIRMATION } from './schemas/confirmation.js';
|
|
3
|
+
import { isErrorOutput } from './toolOutput.js';
|
|
3
4
|
import { adminApiPrefix, CONFIRMATION_PAGE_PATH } from './urls.js';
|
|
4
5
|
export const WAIT_FOR_CONFIRMATION_TOOL_NAME = 'wait_for_confirmation';
|
|
5
6
|
const CONFIRMATION_TTL_MS = 10 * 60 * 1000;
|
|
@@ -208,7 +209,7 @@ export class McpAdminPanelConfirmation {
|
|
|
208
209
|
clearTimeout(timer);
|
|
209
210
|
input.abortSignal?.removeEventListener('abort', abort);
|
|
210
211
|
}
|
|
211
|
-
return { output, isError: responseStatus
|
|
212
|
+
return { output, isError: isErrorOutput(responseStatus, output) };
|
|
212
213
|
}
|
|
213
214
|
/** All actions of a group belong to one user and run as one agent, so they can share one verification. */
|
|
214
215
|
openGroup(ids, adminUser) {
|
package/dist/apiTools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compactInputSchema } from './compactInputSchema.js';
|
|
2
|
+
import { isErrorOutput } from './toolOutput.js';
|
|
2
3
|
import { adminApiPrefix } from './urls.js';
|
|
3
4
|
import { AdminForthDataTypes } from 'adminforth';
|
|
4
5
|
const METHODS_WITHOUT_REQUEST_BODY = new Set(['GET', 'HEAD']);
|
|
@@ -308,7 +309,7 @@ export class AdminForthApiTools {
|
|
|
308
309
|
isError: true,
|
|
309
310
|
};
|
|
310
311
|
}
|
|
311
|
-
const isError = response.status
|
|
312
|
+
const isError = isErrorOutput(response.status, output);
|
|
312
313
|
return {
|
|
313
314
|
output: isError || !override?.project ? output : override.project(output, args, params.arguments ?? {}),
|
|
314
315
|
isError,
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="af-mcp-authorize flex min-h-screen items-center justify-center bg-
|
|
3
|
-
<
|
|
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="
|
|
6
|
-
<p class="
|
|
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="
|
|
12
|
+
<h1 :class="TITLE_CLASSES">
|
|
12
13
|
{{ $t('Connect {client}?', { client: authorization.clientName }) }}
|
|
13
14
|
</h1>
|
|
14
|
-
<p class="
|
|
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
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
39
|
-
<h1 class="
|
|
40
|
-
<p class="
|
|
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
|
-
|
|
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-
|
|
3
|
-
<
|
|
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="
|
|
6
|
-
<p class="
|
|
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
|
|
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-
|
|
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="
|
|
19
|
-
<p class="
|
|
22
|
+
<h1 :class="TITLE_CLASSES">{{ $t('Cannot run the action') }}</h1>
|
|
23
|
+
<p :class="TEXT_CLASSES">{{ error }}</p>
|
|
20
24
|
</template>
|
|
21
|
-
</
|
|
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
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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-
|
|
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="
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
79
|
-
<
|
|
80
|
-
<
|
|
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
|
-
</
|
|
83
|
-
<
|
|
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
|
-
</
|
|
86
|
-
</
|
|
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-
|
|
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
|
|
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-
|
|
102
|
-
<label class="mt-5 block text-sm font-medium text-
|
|
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-
|
|
113
|
-
<pre class="mt-2 whitespace-pre-wrap
|
|
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: '
|
|
82
|
-
pageLabel: 'MCP
|
|
81
|
+
icon: 'lucide:bot',
|
|
82
|
+
pageLabel: 'MCP',
|
|
83
83
|
slug: 'mcp',
|
|
84
84
|
component: this.componentPath('McpSettings.vue'),
|
|
85
85
|
isVisible: () => true,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isErrorOutput(status: number, output: unknown): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// AdminForth core reports a beforeSave hook that returned { ok: false, error: null } with this error.
|
|
2
|
+
// The hook saved the record itself and may point to it with redirectToRecordId; the admin panel treats it as a success.
|
|
3
|
+
const HOOK_ABORT_ERROR = 'Operation aborted by hook';
|
|
4
|
+
export function isErrorOutput(status, output) {
|
|
5
|
+
if (status >= 400)
|
|
6
|
+
return true;
|
|
7
|
+
if (!output || typeof output !== 'object')
|
|
8
|
+
return false;
|
|
9
|
+
const { error, ok } = output;
|
|
10
|
+
if (error === HOOK_ABORT_ERROR)
|
|
11
|
+
return false;
|
|
12
|
+
return 'error' in output || ok === false;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/mcp",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
52
|
+
"adminforth": "^4.1.0",
|
|
53
53
|
"express": "^4.21.0 || ^5.0.0"
|
|
54
54
|
},
|
|
55
55
|
"release": {
|