@adminforth/mcp 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devforth.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # AdminForth MCP
2
+
3
+ `@adminforth/mcp` exposes the AdminForth API tool surface as an authenticated remote MCP server. Each MCP auth secret belongs to one AdminForth user, so resource permissions and hooks are preserved.
4
+
5
+ The server identifies itself to agents using the configured AdminForth `brandName`. An optional `adminPanelOrigin` can distinguish deployments further; the plugin combines it with the AdminForth `baseUrl`.
6
+
7
+ Install with:
8
+
9
+ ```bash
10
+ pnpm add @adminforth/mcp
11
+ ```
12
+
13
+ See the [AdminForth MCP documentation](https://adminforth.dev/docs/tutorial/Plugins/mcp/) for the auth secret table and plugin configuration.
@@ -0,0 +1,27 @@
1
+ import type { AdminUser, IAdminForth } from 'adminforth';
2
+ export interface McpToolDefinition {
3
+ name: string;
4
+ description?: string;
5
+ inputSchema: Record<string, unknown>;
6
+ annotations?: {
7
+ destructiveHint: boolean;
8
+ };
9
+ }
10
+ export declare class AdminForthApiTools {
11
+ private readonly adminforth;
12
+ private readonly hiddenResourceIds;
13
+ constructor(adminforth: IAdminForth, hiddenResourceIds: ReadonlySet<string>);
14
+ private schemas;
15
+ list(): McpToolDefinition[];
16
+ call(params: {
17
+ name: string;
18
+ arguments?: Record<string, unknown>;
19
+ adminUser: AdminUser;
20
+ headers: Record<string, any>;
21
+ requestUrl: string;
22
+ abortSignal: AbortSignal;
23
+ }): Promise<{
24
+ output: unknown;
25
+ isError: boolean;
26
+ }>;
27
+ }
@@ -0,0 +1,127 @@
1
+ const METHODS_WITHOUT_REQUEST_BODY = new Set(['GET', 'HEAD']);
2
+ function isRegisteredToolSchema(schema) {
3
+ return typeof schema.handler === 'function';
4
+ }
5
+ function endpointPathToToolName(path) {
6
+ return path
7
+ .replace(/^\/+/, '')
8
+ .replace(/[^a-zA-Z0-9_]+/g, '_')
9
+ .replace(/^_+|_+$/g, '');
10
+ }
11
+ function stripAdminApiPrefix(path, adminforth) {
12
+ const baseUrl = (adminforth.config.baseUrl ?? '').replace(/\/$/, '');
13
+ const apiPrefix = `${baseUrl}/adminapi/v1`;
14
+ const strippedPath = path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) : path;
15
+ return strippedPath.startsWith('/') ? strippedPath : `/${strippedPath}`;
16
+ }
17
+ function createDirectResponse() {
18
+ return {
19
+ status: 200,
20
+ setHeader() { },
21
+ setStatus(status, message) {
22
+ this.status = status;
23
+ this.message = message;
24
+ },
25
+ blobStream() {
26
+ throw new Error('File streaming is not available through MCP tools');
27
+ },
28
+ };
29
+ }
30
+ export class AdminForthApiTools {
31
+ adminforth;
32
+ hiddenResourceIds;
33
+ constructor(adminforth, hiddenResourceIds) {
34
+ this.adminforth = adminforth;
35
+ this.hiddenResourceIds = hiddenResourceIds;
36
+ }
37
+ schemas() {
38
+ const schemas = new Map();
39
+ for (const schema of this.adminforth.openApi.registeredSchemas) {
40
+ if (!isRegisteredToolSchema(schema))
41
+ continue;
42
+ const path = stripAdminApiPrefix(schema.path, this.adminforth);
43
+ if (path === '/mcp' || path.startsWith('/mcp/'))
44
+ continue;
45
+ schemas.set(endpointPathToToolName(path), schema);
46
+ }
47
+ return schemas;
48
+ }
49
+ list() {
50
+ return Array.from(this.schemas().entries())
51
+ .sort(([left], [right]) => left.localeCompare(right))
52
+ .map(([name, schema]) => ({
53
+ name,
54
+ ...(schema.description && { description: schema.description }),
55
+ inputSchema: schema.request_schema ?? {
56
+ type: 'object',
57
+ properties: {},
58
+ additionalProperties: true,
59
+ },
60
+ ...(schema.agent?.isDangerous && {
61
+ annotations: { destructiveHint: true },
62
+ }),
63
+ }));
64
+ }
65
+ async call(params) {
66
+ const schema = this.schemas().get(params.name);
67
+ if (!schema) {
68
+ return { output: { error: `Unknown tool: ${params.name}` }, isError: true };
69
+ }
70
+ if (typeof params.arguments?.resourceId === 'string'
71
+ && this.hiddenResourceIds.has(params.arguments.resourceId)) {
72
+ return {
73
+ output: { error: 'This resource is not available through MCP.' },
74
+ isError: true,
75
+ };
76
+ }
77
+ const method = schema.method.toUpperCase();
78
+ const hasBody = !METHODS_WITHOUT_REQUEST_BODY.has(method);
79
+ const body = hasBody ? (params.arguments ?? {}) : {};
80
+ const query = hasBody ? {} : (params.arguments ?? {});
81
+ const requestValidation = this.adminforth.openApi.validateRequestSchema(schema, body);
82
+ if (!requestValidation.valid) {
83
+ return {
84
+ output: { error: 'REQUEST_VALIDATION_FAILED', details: requestValidation.errors },
85
+ isError: true,
86
+ };
87
+ }
88
+ const response = createDirectResponse();
89
+ const language = String(params.headers['accept-language'] ?? 'en');
90
+ const output = await schema.handler({
91
+ body,
92
+ query,
93
+ headers: params.headers,
94
+ cookies: [],
95
+ adminUser: params.adminUser,
96
+ response,
97
+ requestUrl: params.requestUrl,
98
+ abortSignal: params.abortSignal,
99
+ _raw_express_req: undefined,
100
+ _raw_express_res: undefined,
101
+ tr: (message, category, translationParams, pluralizationNumber) => this.adminforth.tr(message, category, language, translationParams, pluralizationNumber),
102
+ });
103
+ if (response.message) {
104
+ return {
105
+ output: response.message,
106
+ isError: response.status >= 400,
107
+ };
108
+ }
109
+ if (output === undefined) {
110
+ return {
111
+ output: { error: 'Tool handler completed without returning a response.' },
112
+ isError: true,
113
+ };
114
+ }
115
+ const responseValidation = this.adminforth.openApi.validateResponseSchema(schema, output);
116
+ if (!responseValidation.valid) {
117
+ return {
118
+ output: { error: 'RESPONSE_VALIDATION_FAILED', details: responseValidation.errors },
119
+ isError: true,
120
+ };
121
+ }
122
+ return {
123
+ output,
124
+ isError: response.status >= 400 || Boolean(output && typeof output === 'object' && 'error' in output),
125
+ };
126
+ }
127
+ }
@@ -0,0 +1,35 @@
1
+ import { type AdminUser, type HttpExtra, type IAdminForth } from 'adminforth';
2
+ import type { McpAuthSecretResourceOptions, McpClientInfo } from './types.js';
3
+ type AdminUserWithExecutor = AdminUser & {
4
+ executedBy?: string;
5
+ };
6
+ export interface AuthenticatedMcpSecret {
7
+ adminUser: AdminUserWithExecutor;
8
+ client: McpClientInfo | null;
9
+ name: string;
10
+ recordId: string;
11
+ }
12
+ export declare class McpAuthSecretStore {
13
+ private readonly adminforth;
14
+ private readonly options;
15
+ private readonly resource;
16
+ constructor(adminforth: IAdminForth, options: McpAuthSecretResourceOptions);
17
+ list(adminUser: AdminUser): Promise<{
18
+ id: any;
19
+ name: any;
20
+ createdAt: any;
21
+ lastUsedAt: any;
22
+ lastUsedByAgent: any;
23
+ }[]>;
24
+ create(name: string, adminUser: AdminUser, extra: HttpExtra): Promise<{
25
+ error: string;
26
+ secret?: undefined;
27
+ } | {
28
+ secret: string;
29
+ error?: undefined;
30
+ }>;
31
+ revoke(id: string, adminUser: AdminUser, extra: HttpExtra): Promise<import("adminforth").DeleteResourceRecordResult>;
32
+ authenticate(secret: string): Promise<AuthenticatedMcpSecret | null>;
33
+ touch(recordId: string, client: McpClientInfo): void;
34
+ }
35
+ export {};
@@ -0,0 +1,98 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
+ import { Filters, Sorts, logger, } from 'adminforth';
3
+ import { UNKNOWN_CLIENT } from './clientInfo.js';
4
+ const SECRET_PREFIX = 'afmcp_';
5
+ function hashSecret(secret) {
6
+ return createHash('sha256').update(secret).digest('hex');
7
+ }
8
+ export class McpAuthSecretStore {
9
+ adminforth;
10
+ options;
11
+ resource;
12
+ constructor(adminforth, options) {
13
+ this.adminforth = adminforth;
14
+ this.options = options;
15
+ this.resource = adminforth.config.resources.find((resource) => resource.resourceId === options.resourceId);
16
+ }
17
+ async list(adminUser) {
18
+ const fields = this.options;
19
+ const records = await this.adminforth.resource(fields.resourceId).list(Filters.EQ(fields.userIdField, adminUser.pk), undefined, undefined, Sorts.DESC(fields.createdAtField));
20
+ return records.map((record) => ({
21
+ id: record[fields.idField],
22
+ name: record[fields.nameField],
23
+ createdAt: record[fields.createdAtField],
24
+ lastUsedAt: record[fields.lastUsedAtField] ?? null,
25
+ lastUsedByAgent: record[fields.lastUsedByAgentField]
26
+ ? JSON.parse(record[fields.lastUsedByAgentField])
27
+ : null,
28
+ }));
29
+ }
30
+ async create(name, adminUser, extra) {
31
+ const fields = this.options;
32
+ const secret = `${SECRET_PREFIX}${randomBytes(32).toString('base64url')}`;
33
+ const result = await this.adminforth.createResourceRecord({
34
+ resource: this.resource,
35
+ adminUser,
36
+ extra,
37
+ record: {
38
+ [fields.idField]: randomUUID(),
39
+ [fields.nameField]: name,
40
+ [fields.secretHashField]: hashSecret(secret),
41
+ [fields.userIdField]: adminUser.pk,
42
+ [fields.createdAtField]: new Date().toISOString(),
43
+ [fields.lastUsedAtField]: null,
44
+ [fields.lastUsedByAgentField]: null,
45
+ },
46
+ });
47
+ return result.error ? { error: result.error } : { secret };
48
+ }
49
+ async revoke(id, adminUser, extra) {
50
+ const fields = this.options;
51
+ const record = await this.adminforth.resource(fields.resourceId).get(Filters.AND(Filters.EQ(fields.idField, id), Filters.EQ(fields.userIdField, adminUser.pk)));
52
+ if (!record)
53
+ return { error: 'MCP auth secret not found' };
54
+ return this.adminforth.deleteResourceRecord({
55
+ resource: this.resource,
56
+ record,
57
+ recordId: id,
58
+ adminUser,
59
+ extra,
60
+ });
61
+ }
62
+ async authenticate(secret) {
63
+ const fields = this.options;
64
+ const record = await this.adminforth.resource(fields.resourceId).get(Filters.EQ(fields.secretHashField, hashSecret(secret)));
65
+ if (!record)
66
+ return null;
67
+ const auth = this.adminforth.config.auth;
68
+ const usersResource = this.adminforth.config.resources.find((resource) => resource.resourceId === auth.usersResourceId);
69
+ const userPrimaryKeyField = usersResource.columns.find((column) => column.primaryKey).name;
70
+ const dbUser = await this.adminforth.resource(usersResource.resourceId).get(Filters.EQ(userPrimaryKeyField, record[fields.userIdField]));
71
+ if (!dbUser)
72
+ return null;
73
+ return {
74
+ adminUser: {
75
+ pk: record[fields.userIdField],
76
+ username: dbUser[auth.usernameField],
77
+ dbUser,
78
+ },
79
+ client: record[fields.lastUsedByAgentField]
80
+ ? JSON.parse(record[fields.lastUsedByAgentField])
81
+ : null,
82
+ name: record[fields.nameField],
83
+ recordId: record[fields.idField],
84
+ };
85
+ }
86
+ touch(recordId, client) {
87
+ const fields = this.options;
88
+ const updates = {
89
+ [fields.lastUsedAtField]: new Date().toISOString(),
90
+ };
91
+ if (client.client !== UNKNOWN_CLIENT) {
92
+ updates[fields.lastUsedByAgentField] = JSON.stringify(client);
93
+ }
94
+ void this.adminforth.resource(fields.resourceId).update(recordId, updates).catch((error) => {
95
+ logger.error(`AdminForthMcpPlugin: failed to update MCP auth secret usage: ${String(error)}`);
96
+ });
97
+ }
98
+ }
@@ -0,0 +1,5 @@
1
+ import type { McpClientInfo } from './types.js';
2
+ export declare const UNKNOWN_CLIENT = "unknown-agent";
3
+ export declare function readMcpClient(body: Record<string, any>, headers: Record<string, any>): McpClientInfo;
4
+ export declare function canonicalAgentName(client: string): string;
5
+ export declare function formatMcpExecutedBy(client: McpClientInfo, authSecretName: string): string;
@@ -0,0 +1,40 @@
1
+ const CLIENT_INFO_META_KEY = 'io.modelcontextprotocol/clientInfo';
2
+ const NON_CLIENT_NAME_CHARACTER_RE = /[^a-z0-9]+/g;
3
+ const EDGE_HYPHEN_RE = /^-+|-+$/g;
4
+ const USER_AGENT_PRODUCT_RE = /^([^/\s]+)(?:\/(\S+))?/;
5
+ export const UNKNOWN_CLIENT = 'unknown-agent';
6
+ function normalizeClientName(name) {
7
+ return name.toLowerCase().replace(NON_CLIENT_NAME_CHARACTER_RE, '-').replace(EDGE_HYPHEN_RE, '');
8
+ }
9
+ function parseUserAgent(userAgent) {
10
+ const [, name, version] = userAgent.match(USER_AGENT_PRODUCT_RE) ?? [];
11
+ return name ? { client: normalizeClientName(name), ver: version ?? null } : null;
12
+ }
13
+ export function readMcpClient(body, headers) {
14
+ const clientInfo = body.params?._meta?.[CLIENT_INFO_META_KEY] ?? (body.method === 'initialize' ? body.params?.clientInfo : undefined);
15
+ if (clientInfo?.name) {
16
+ return {
17
+ client: normalizeClientName(clientInfo.name),
18
+ ver: clientInfo.version ?? null,
19
+ };
20
+ }
21
+ return parseUserAgent(String(headers['user-agent'] ?? '')) ?? {
22
+ client: UNKNOWN_CLIENT,
23
+ ver: null,
24
+ };
25
+ }
26
+ export function canonicalAgentName(client) {
27
+ if (client.includes('claude') && client.includes('code'))
28
+ return 'claude-code';
29
+ if (client.includes('claude'))
30
+ return 'claude';
31
+ if (client.includes('codex') || client.includes('openai') || client === 'undici')
32
+ return 'codex';
33
+ if (client.includes('gemini'))
34
+ return 'gemini';
35
+ return client;
36
+ }
37
+ export function formatMcpExecutedBy(client, authSecretName) {
38
+ const version = client.ver ? `@${client.ver}` : '';
39
+ return `${canonicalAgentName(client.client)}${version} | ${authSecretName}`;
40
+ }
@@ -0,0 +1,230 @@
1
+ <template>
2
+ <div class="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('Create a separate auth secret for each AI agent. Secrets are shown only once.') }}
10
+ </p>
11
+ </div>
12
+ <button
13
+ type="button"
14
+ class="rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700"
15
+ @click="openCreateDialog"
16
+ >
17
+ {{ $t('Create auth secret') }}
18
+ </button>
19
+ </div>
20
+
21
+ <div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
22
+ <p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('MCP server URL') }}</p>
23
+ <div class="mt-2 flex items-center gap-2">
24
+ <code class="min-w-0 flex-1 overflow-x-auto rounded bg-white px-3 py-2 text-sm text-gray-800 dark:bg-gray-900 dark:text-gray-100">{{ mcpUrl }}</code>
25
+ <button class="text-sm font-medium text-blue-700 dark:text-blue-400" @click="copy(mcpUrl)">
26
+ {{ $t('Copy') }}
27
+ </button>
28
+ </div>
29
+ <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
30
+ <code>Authorization: Bearer &lt;secret&gt;</code>
31
+ </p>
32
+ </div>
33
+
34
+ <div v-if="authSecrets.length" class="mt-6 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
35
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
36
+ <thead class="bg-gray-50 dark:bg-gray-800">
37
+ <tr>
38
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Name') }}</th>
39
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Last used by') }}</th>
40
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Last usage') }}</th>
41
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Actions') }}</th>
42
+ </tr>
43
+ </thead>
44
+ <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
45
+ <tr v-for="authSecret in authSecrets" :key="authSecret.id">
46
+ <td class="px-4 py-3">
47
+ <p class="font-medium text-gray-900 dark:text-white">{{ authSecret.name }}</p>
48
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ formatDate(authSecret.createdAt) }}</p>
49
+ </td>
50
+ <td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200">
51
+ <div v-if="authSecret.lastUsedByAgent" class="flex items-center gap-2">
52
+ <img v-if="describeAgent(authSecret.lastUsedByAgent).icon" :src="describeAgent(authSecret.lastUsedByAgent).icon" class="h-5 w-5" alt="" />
53
+ <span>{{ describeAgent(authSecret.lastUsedByAgent).label }}</span>
54
+ </div>
55
+ <span v-else class="text-gray-400">{{ $t('Not used yet') }}</span>
56
+ </td>
57
+ <td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
58
+ {{ authSecret.lastUsedAt ? formatDate(authSecret.lastUsedAt) : $t('Never') }}
59
+ </td>
60
+ <td class="px-4 py-3 text-right">
61
+ <button
62
+ type="button"
63
+ class="rounded-md border border-red-300 px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 disabled:opacity-50 dark:border-red-700 dark:text-red-300 dark:hover:bg-red-950"
64
+ :disabled="revokingId === authSecret.id"
65
+ @click="revoke(authSecret)"
66
+ >
67
+ {{ revokingId === authSecret.id ? $t('Revoking') : $t('Revoke') }}
68
+ </button>
69
+ </td>
70
+ </tr>
71
+ </tbody>
72
+ </table>
73
+ </div>
74
+ <p v-else-if="!loading" class="mt-6 text-sm text-gray-500 dark:text-gray-400">
75
+ {{ $t('No MCP auth secrets yet.') }}
76
+ </p>
77
+
78
+ <div v-if="dialogOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
79
+ <div class="w-full max-w-xl rounded-xl bg-white p-6 shadow-xl dark:bg-gray-800">
80
+ <template v-if="!createdSecret">
81
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white">{{ $t('Create MCP auth secret') }}</h3>
82
+ <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $t('Use one auth secret for one agent.') }}</p>
83
+ <label class="mt-5 block text-sm font-medium text-gray-700 dark:text-gray-200">{{ $t('Secret name') }}</label>
84
+ <input
85
+ v-model="secretName"
86
+ class="mt-2 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
87
+ placeholder="Claude Code"
88
+ @keydown.enter="createAuthSecret"
89
+ />
90
+ <div class="mt-6 flex justify-end gap-3">
91
+ <button class="px-4 py-2 text-sm text-gray-600 dark:text-gray-300" @click="closeDialog">{{ $t('Cancel') }}</button>
92
+ <button
93
+ class="rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white disabled:opacity-50 dark:bg-blue-600"
94
+ :disabled="creating || !secretName.trim()"
95
+ @click="createAuthSecret"
96
+ >
97
+ {{ creating ? $t('Creating') : $t('Create') }}
98
+ </button>
99
+ </div>
100
+ </template>
101
+ <template v-else>
102
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white">{{ $t('Auth secret created') }}</h3>
103
+ <p class="mt-2 text-sm text-amber-700 dark:text-amber-300">{{ $t('Copy it now. You will not be able to view it again.') }}</p>
104
+ <code class="mt-4 block overflow-x-auto rounded-lg bg-gray-100 p-3 text-sm text-gray-900 dark:bg-gray-900 dark:text-gray-100">{{ createdSecret }}</code>
105
+ <p class="mt-5 text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Prompt for your agent') }}</p>
106
+ <pre class="mt-2 whitespace-pre-wrap rounded-lg bg-gray-100 p-3 text-sm text-gray-800 dark:bg-gray-900 dark:text-gray-100">{{ setupPrompt }}</pre>
107
+ <div class="mt-6 flex justify-end gap-3">
108
+ <button class="text-sm font-medium text-blue-700 dark:text-blue-400" @click="copy(setupPrompt)">{{ $t('Copy prompt') }}</button>
109
+ <button class="rounded-lg bg-blue-700 px-4 py-2 text-sm font-medium text-white dark:bg-blue-600" @click="closeDialog">{{ $t('Done') }}</button>
110
+ </div>
111
+ </template>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </template>
116
+
117
+ <script setup lang="ts">
118
+ import { computed, onMounted, ref } from 'vue';
119
+ import { callAdminForthApi } from '@/utils';
120
+ import claudeCodeIcon from './icons/claude-code.svg';
121
+ import codexIcon from './icons/codex.svg';
122
+ import geminiIcon from './icons/gemini.svg';
123
+
124
+ type Agent = { client: string; ver: string | null };
125
+ type AuthSecret = {
126
+ id: string;
127
+ name: string;
128
+ createdAt: string;
129
+ lastUsedAt: string | null;
130
+ lastUsedByAgent: Agent | null;
131
+ };
132
+
133
+ const authSecrets = ref<AuthSecret[]>([]);
134
+ const loading = ref(true);
135
+ const dialogOpen = ref(false);
136
+ const secretName = ref('');
137
+ const createdSecret = ref('');
138
+ const creating = ref(false);
139
+ const revokingId = ref<string | null>(null);
140
+
141
+ const mcpUrl = computed(() => {
142
+ const baseUrl = (import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || '').replace(/\/$/, '');
143
+ return `${window.location.origin}${baseUrl}/adminapi/v1/mcp`;
144
+ });
145
+ const setupPrompt = computed(() => [
146
+ 'Add this remote MCP server to the current agent:',
147
+ `URL: ${mcpUrl.value}`,
148
+ `Header: Authorization: Bearer ${createdSecret.value || '<secret>'}`,
149
+ 'Use this auth secret only for this agent.',
150
+ ].join('\n'));
151
+
152
+ onMounted(loadAuthSecrets);
153
+
154
+ async function loadAuthSecrets() {
155
+ loading.value = true;
156
+ try {
157
+ const response = await callAdminForthApi({ method: 'GET', path: '/mcp/auth-secrets' });
158
+ if (response) authSecrets.value = response.authSecrets;
159
+ } finally {
160
+ loading.value = false;
161
+ }
162
+ }
163
+
164
+ function openCreateDialog() {
165
+ secretName.value = '';
166
+ createdSecret.value = '';
167
+ dialogOpen.value = true;
168
+ }
169
+
170
+ function closeDialog() {
171
+ dialogOpen.value = false;
172
+ createdSecret.value = '';
173
+ }
174
+
175
+ async function createAuthSecret() {
176
+ creating.value = true;
177
+ try {
178
+ const response = await callAdminForthApi({
179
+ method: 'POST',
180
+ path: '/mcp/auth-secrets',
181
+ body: { name: secretName.value.trim() },
182
+ });
183
+ if (response?.secret) {
184
+ createdSecret.value = response.secret;
185
+ await loadAuthSecrets();
186
+ }
187
+ } finally {
188
+ creating.value = false;
189
+ }
190
+ }
191
+
192
+ async function revoke(authSecret: AuthSecret) {
193
+ if (!window.confirm(`Revoke MCP auth secret "${authSecret.name}"?`)) return;
194
+ revokingId.value = authSecret.id;
195
+ try {
196
+ await callAdminForthApi({ method: 'DELETE', path: '/mcp/auth-secrets', body: { id: authSecret.id } });
197
+ await loadAuthSecrets();
198
+ } finally {
199
+ revokingId.value = null;
200
+ }
201
+ }
202
+
203
+ function describeAgent(agent: Agent) {
204
+ const client = agent.client;
205
+ let name = client.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
206
+ let icon: string | null = null;
207
+ if (client.includes('claude') && client.includes('code')) {
208
+ name = 'Claude Code';
209
+ icon = claudeCodeIcon;
210
+ } else if (client.includes('claude')) {
211
+ name = 'Claude';
212
+ icon = claudeCodeIcon;
213
+ } else if (client.includes('codex') || client.includes('openai') || client === 'undici') {
214
+ name = 'Codex';
215
+ icon = codexIcon;
216
+ } else if (client.includes('gemini')) {
217
+ name = 'Gemini';
218
+ icon = geminiIcon;
219
+ }
220
+ return { label: agent.ver ? `${name} v${agent.ver}` : name, icon };
221
+ }
222
+
223
+ function formatDate(value: string) {
224
+ return new Date(value).toLocaleString();
225
+ }
226
+
227
+ async function copy(value: string) {
228
+ await navigator.clipboard.writeText(value);
229
+ }
230
+ </script>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#d97757" fill-rule="evenodd" d="M21 10.95h3v3.1h-3v3.03h-1.49V20H18v-2.92h-1.49V20H15v-2.92H9V20H7.49v-2.92H6V20H4.49v-2.92H3v-3.03H0v-3.1h3V5h18zM6 10.95h1.49V8.1H6zm10.51 0H18V8.1h-1.49z" clip-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#2f80ed" fill-rule="evenodd" d="M8.09.46a6.1 6.1 0 0 1 6.61 1.28 6.1 6.1 0 0 1 7.54 7.46 6.1 6.1 0 0 1-2.79 10.26 6.1 6.1 0 0 1-10.22 2.78 6.1 6.1 0 0 1-7.53-7.43A6.1 6.1 0 0 1 4.55 4.53 6 6 0 0 1 8.09.46M7.28 8.31a.85.85 0 0 0-1.47.84l1.69 2.96-1.69 2.85a.85.85 0 0 0 1.46.87l1.94-3.28a.85.85 0 0 0 .01-.85zm5.45 6.24a.85.85 0 0 0 0 1.69h4.85a.85.85 0 0 0 0-1.69z" clip-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="g" x1="3" y1="20" x2="21" y2="4" gradientUnits="userSpaceOnUse"><stop stop-color="#14b86e"/><stop offset=".45" stop-color="#3689ff"/><stop offset="1" stop-color="#fa4340"/></linearGradient></defs><path fill="url(#g)" d="M12 0c.7 6.8 5.2 11.3 12 12-6.8.7-11.3 5.2-12 12C11.3 17.2 6.8 12.7 0 12 6.8 11.3 11.3 6.8 12 0"/></svg>
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ "../node_modules/adminforth/dist/spa/src/*"
7
+ ],
8
+ "*": [
9
+ "../node_modules/adminforth/dist/spa/node_modules/*"
10
+ ],
11
+ "@@/*": [
12
+ "."
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,18 @@
1
+ import { AdminForthPlugin, type IAdminForth, type IHttpServer } from 'adminforth';
2
+ import type { PluginOptions } from './types.js';
3
+ export default class AdminForthMcpPlugin extends AdminForthPlugin {
4
+ options: PluginOptions;
5
+ pluginsScope: 'global';
6
+ private authSecretStore;
7
+ private apiTools;
8
+ private serverPresentation;
9
+ constructor(options: PluginOptions);
10
+ instanceUniqueRepresentation(): string;
11
+ modifyGlobalConfig(adminforth: IAdminForth): void;
12
+ private validateAuthSecretResource;
13
+ setupEndpoints(server: IHttpServer): void;
14
+ private handleMcpRequest;
15
+ }
16
+ export type { McpAuthSecretResourceOptions, PluginOptions } from './types.js';
17
+ export { canonicalAgentName, formatMcpExecutedBy, readMcpClient } from './clientInfo.js';
18
+ export { handleMcpProtocol } from './mcpProtocol.js';
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ import { AdminForthPlugin, } from 'adminforth';
2
+ import { AdminForthApiTools } from './apiTools.js';
3
+ import { formatMcpExecutedBy, readMcpClient, UNKNOWN_CLIENT } from './clientInfo.js';
4
+ import { createMcpServerPresentation, handleMcpProtocol, } from './mcpProtocol.js';
5
+ import { McpAuthSecretStore } from './authSecretStore.js';
6
+ const BEARER_SECRET_RE = /^Bearer (afmcp_[A-Za-z0-9_-]+)$/i;
7
+ const MCP_PATH = '/mcp';
8
+ function requestExtra(input) {
9
+ return {
10
+ body: input.body,
11
+ query: input.query,
12
+ headers: input.headers,
13
+ cookies: input.cookies,
14
+ requestUrl: input.requestUrl,
15
+ response: input.response,
16
+ };
17
+ }
18
+ function jsonRpcAuthError(id = null) {
19
+ return {
20
+ jsonrpc: '2.0',
21
+ id,
22
+ error: { code: -32001, message: 'Invalid or revoked MCP auth secret.' },
23
+ };
24
+ }
25
+ export default class AdminForthMcpPlugin extends AdminForthPlugin {
26
+ options;
27
+ pluginsScope = 'global';
28
+ authSecretStore;
29
+ apiTools;
30
+ serverPresentation;
31
+ constructor(options) {
32
+ super(options, import.meta.url);
33
+ this.options = options;
34
+ this.shouldHaveSingleInstancePerWholeApp = () => true;
35
+ }
36
+ instanceUniqueRepresentation() {
37
+ return 'single';
38
+ }
39
+ modifyGlobalConfig(adminforth) {
40
+ super.modifyGlobalConfig(adminforth);
41
+ const authSecretResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.authSecretResource.resourceId);
42
+ if (!authSecretResource) {
43
+ throw new Error(`AdminForthMcpPlugin: auth secret resource "${this.options.authSecretResource.resourceId}" not found`);
44
+ }
45
+ this.validateAuthSecretResource(authSecretResource);
46
+ const secretHashColumn = authSecretResource.columns.find((column) => column.name === this.options.authSecretResource.secretHashField);
47
+ secretHashColumn.backendOnly = true;
48
+ secretHashColumn.showIn = {
49
+ show: false,
50
+ list: false,
51
+ create: false,
52
+ edit: false,
53
+ filter: false,
54
+ };
55
+ const auth = adminforth.config.auth;
56
+ auth.userMenuSettingsPages ??= [];
57
+ if (!auth.userMenuSettingsPages.some((page) => page.slug === 'mcp')) {
58
+ auth.userMenuSettingsPages.push({
59
+ icon: 'flowbite:server-outline',
60
+ pageLabel: 'MCP Settings',
61
+ slug: 'mcp',
62
+ component: this.componentPath('McpSettings.vue'),
63
+ isVisible: () => true,
64
+ });
65
+ }
66
+ this.authSecretStore = new McpAuthSecretStore(adminforth, this.options.authSecretResource);
67
+ this.apiTools = new AdminForthApiTools(adminforth, new Set([this.options.authSecretResource.resourceId]));
68
+ this.serverPresentation = createMcpServerPresentation(adminforth.config.customization.brandName, this.options.adminPanelOrigin, adminforth.config.baseUrl);
69
+ }
70
+ validateAuthSecretResource(resource) {
71
+ const fields = this.options.authSecretResource;
72
+ for (const fieldName of [
73
+ fields.idField,
74
+ fields.nameField,
75
+ fields.secretHashField,
76
+ fields.userIdField,
77
+ fields.createdAtField,
78
+ fields.lastUsedAtField,
79
+ fields.lastUsedByAgentField,
80
+ ]) {
81
+ if (!resource.columns.some((column) => column.name === fieldName)) {
82
+ throw new Error(`AdminForthMcpPlugin: column "${fieldName}" not found in auth secret resource "${resource.resourceId}"`);
83
+ }
84
+ }
85
+ if (!resource.columns.find((column) => column.name === fields.idField).primaryKey) {
86
+ throw new Error(`AdminForthMcpPlugin: column "${fields.idField}" must be the primary key of auth secret resource "${resource.resourceId}"`);
87
+ }
88
+ }
89
+ setupEndpoints(server) {
90
+ server.endpoint({
91
+ method: 'GET',
92
+ path: `${MCP_PATH}/auth-secrets`,
93
+ handler: async ({ adminUser }) => ({
94
+ authSecrets: await this.authSecretStore.list(adminUser),
95
+ }),
96
+ });
97
+ server.endpoint({
98
+ method: 'POST',
99
+ path: `${MCP_PATH}/auth-secrets`,
100
+ handler: async (input) => {
101
+ const name = String(input.body.name ?? '').trim();
102
+ if (!name) {
103
+ input.response.setStatus(400);
104
+ return { error: 'Auth secret name is required' };
105
+ }
106
+ return this.authSecretStore.create(name, input.adminUser, requestExtra(input));
107
+ },
108
+ });
109
+ server.endpoint({
110
+ method: 'DELETE',
111
+ path: `${MCP_PATH}/auth-secrets`,
112
+ handler: async (input) => {
113
+ const result = await this.authSecretStore.revoke(String(input.body.id), input.adminUser, requestExtra(input));
114
+ if (result.error)
115
+ input.response.setStatus(404);
116
+ return result;
117
+ },
118
+ });
119
+ server.endpoint({
120
+ method: 'POST',
121
+ path: MCP_PATH,
122
+ noAuth: true,
123
+ handler: async (input) => this.handleMcpRequest(input),
124
+ });
125
+ }
126
+ async handleMcpRequest(input) {
127
+ if (input.headers.origin) {
128
+ input.response.setStatus(403);
129
+ return {
130
+ jsonrpc: '2.0',
131
+ id: input.body.id ?? null,
132
+ error: { code: -32003, message: 'Browser-originated MCP requests are not allowed.' },
133
+ };
134
+ }
135
+ const bearerMatch = String(input.headers.authorization ?? '').match(BEARER_SECRET_RE);
136
+ const authenticated = bearerMatch
137
+ ? await this.authSecretStore.authenticate(bearerMatch[1])
138
+ : null;
139
+ if (!authenticated) {
140
+ input.response.setHeader('WWW-Authenticate', 'Bearer');
141
+ input.response.setStatus(401);
142
+ return jsonRpcAuthError(input.body.id ?? null);
143
+ }
144
+ const authorization = await this.adminforth.auth.runAdminUserAuthorizeHooks(authenticated.adminUser, input.response, requestExtra(input));
145
+ if (!authorization.allowed) {
146
+ input.response.setStatus(403);
147
+ return jsonRpcAuthError(input.body.id ?? null);
148
+ }
149
+ const reportedClient = readMcpClient(input.body, input.headers);
150
+ const client = reportedClient.client === UNKNOWN_CLIENT && authenticated.client
151
+ ? authenticated.client
152
+ : reportedClient;
153
+ this.authSecretStore.touch(authenticated.recordId, reportedClient);
154
+ const adminUser = {
155
+ ...authenticated.adminUser,
156
+ executedBy: formatMcpExecutedBy(client, authenticated.name),
157
+ };
158
+ const protocolResponse = await handleMcpProtocol({
159
+ ...this.serverPresentation,
160
+ body: input.body,
161
+ headers: input.headers,
162
+ listTools: () => this.apiTools.list(),
163
+ callTool: (name, arguments_) => this.apiTools.call({
164
+ name,
165
+ arguments: arguments_,
166
+ adminUser,
167
+ headers: input.headers,
168
+ requestUrl: input.requestUrl,
169
+ abortSignal: input.abortSignal,
170
+ }),
171
+ });
172
+ input.response.setStatus(protocolResponse.status);
173
+ if (!protocolResponse.body) {
174
+ input._raw_express_res.status(protocolResponse.status).end();
175
+ return null;
176
+ }
177
+ return protocolResponse.body;
178
+ }
179
+ }
180
+ export { canonicalAgentName, formatMcpExecutedBy, readMcpClient } from './clientInfo.js';
181
+ export { handleMcpProtocol } from './mcpProtocol.js';
@@ -0,0 +1,31 @@
1
+ import type { McpToolDefinition } from './apiTools.js';
2
+ export declare const MODERN_PROTOCOL_VERSION = "2026-07-28";
3
+ export declare const LEGACY_PROTOCOL_VERSION = "2025-11-25";
4
+ export interface McpServerInfo {
5
+ name: string;
6
+ title: string;
7
+ version: string;
8
+ description: string;
9
+ websiteUrl?: string;
10
+ }
11
+ export interface McpServerPresentation {
12
+ serverInfo: McpServerInfo;
13
+ instructions: string;
14
+ }
15
+ export declare function createMcpServerPresentation(brandName: string, adminPanelOrigin?: string, baseUrl?: string): McpServerPresentation;
16
+ export interface McpProtocolContext {
17
+ body: Record<string, any>;
18
+ headers: Record<string, any>;
19
+ serverInfo: McpServerInfo;
20
+ instructions: string;
21
+ listTools: () => McpToolDefinition[];
22
+ callTool: (name: string, arguments_: Record<string, unknown> | undefined) => Promise<{
23
+ output: unknown;
24
+ isError: boolean;
25
+ }>;
26
+ }
27
+ export interface McpProtocolResponse {
28
+ status: number;
29
+ body?: Record<string, unknown>;
30
+ }
31
+ export declare function handleMcpProtocol(context: McpProtocolContext): Promise<McpProtocolResponse>;
@@ -0,0 +1,137 @@
1
+ export const MODERN_PROTOCOL_VERSION = '2026-07-28';
2
+ export const LEGACY_PROTOCOL_VERSION = '2025-11-25';
3
+ const LEGACY_PROTOCOL_VERSIONS = new Set([
4
+ LEGACY_PROTOCOL_VERSION,
5
+ '2025-06-18',
6
+ '2025-03-26',
7
+ ]);
8
+ const PROTOCOL_VERSION_META_KEY = 'io.modelcontextprotocol/protocolVersion';
9
+ const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapabilities';
10
+ const SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo';
11
+ export function createMcpServerPresentation(brandName, adminPanelOrigin, baseUrl = '') {
12
+ const adminPanelUrl = adminPanelOrigin
13
+ ? new URL(baseUrl || '/', adminPanelOrigin).toString()
14
+ : undefined;
15
+ const adminPanel = `AdminForth admin panel for "${brandName}"${adminPanelUrl ? ` at ${adminPanelUrl}` : ''}`;
16
+ return {
17
+ serverInfo: {
18
+ name: 'adminforth-mcp',
19
+ title: `${brandName} Admin Panel`,
20
+ version: '1.0.0',
21
+ description: `${adminPanel}.`,
22
+ ...(adminPanelUrl && { websiteUrl: adminPanelUrl }),
23
+ },
24
+ instructions: `This is the ${adminPanel}. Use its tools to read and update data allowed for the authenticated user.`,
25
+ };
26
+ }
27
+ function errorResponse(id, code, message, status = 400) {
28
+ return {
29
+ status,
30
+ body: { jsonrpc: '2.0', id, error: { code, message } },
31
+ };
32
+ }
33
+ function isModernRequest(body) {
34
+ return body.params?._meta?.[PROTOCOL_VERSION_META_KEY] === MODERN_PROTOCOL_VERSION;
35
+ }
36
+ function completeResult(result, modern, serverInfo) {
37
+ if (!modern)
38
+ return result;
39
+ return {
40
+ resultType: 'complete',
41
+ ...result,
42
+ _meta: {
43
+ ...result._meta,
44
+ [SERVER_INFO_META_KEY]: serverInfo,
45
+ },
46
+ };
47
+ }
48
+ function successResponse(id, result, modern, serverInfo) {
49
+ return {
50
+ status: 200,
51
+ body: {
52
+ jsonrpc: '2.0',
53
+ id,
54
+ result: completeResult(result, modern, serverInfo),
55
+ },
56
+ };
57
+ }
58
+ function validateModernHeaders(body, headers) {
59
+ const meta = body.params?._meta;
60
+ if (!meta || !(CLIENT_CAPABILITIES_META_KEY in meta)) {
61
+ return errorResponse(body.id ?? null, -32602, 'Missing required MCP request metadata.');
62
+ }
63
+ if (headers['mcp-protocol-version'] !== MODERN_PROTOCOL_VERSION
64
+ || headers['mcp-method'] !== body.method
65
+ || (body.method === 'tools/call' && headers['mcp-name'] !== body.params?.name)) {
66
+ return errorResponse(body.id ?? null, -32020, 'MCP request headers do not match the JSON-RPC body.');
67
+ }
68
+ return null;
69
+ }
70
+ function serializeToolOutput(output) {
71
+ return typeof output === 'string' ? output : JSON.stringify(output, null, 2);
72
+ }
73
+ export async function handleMcpProtocol(context) {
74
+ const { body, headers, serverInfo, instructions } = context;
75
+ if (body.jsonrpc !== '2.0' || typeof body.method !== 'string') {
76
+ return errorResponse(body.id ?? null, -32600, 'Invalid JSON-RPC request.');
77
+ }
78
+ const modern = isModernRequest(body);
79
+ const requestedVersion = body.params?._meta?.[PROTOCOL_VERSION_META_KEY];
80
+ if (requestedVersion && requestedVersion !== MODERN_PROTOCOL_VERSION) {
81
+ return errorResponse(body.id ?? null, -32000, `Unsupported MCP protocol version: ${requestedVersion}`);
82
+ }
83
+ if (modern) {
84
+ const headerError = validateModernHeaders(body, headers);
85
+ if (headerError)
86
+ return headerError;
87
+ }
88
+ if (body.id === undefined) {
89
+ return { status: 202 };
90
+ }
91
+ if (typeof body.id !== 'string' && typeof body.id !== 'number') {
92
+ return errorResponse(null, -32600, 'JSON-RPC id must be a string or number.');
93
+ }
94
+ switch (body.method) {
95
+ case 'server/discover':
96
+ if (!modern) {
97
+ return errorResponse(body.id, -32602, 'server/discover requires modern MCP request metadata.');
98
+ }
99
+ return successResponse(body.id, {
100
+ supportedVersions: [MODERN_PROTOCOL_VERSION],
101
+ capabilities: { tools: {} },
102
+ instructions,
103
+ ttlMs: 0,
104
+ cacheScope: 'private',
105
+ }, true, serverInfo);
106
+ case 'initialize': {
107
+ const requestedVersion = body.params?.protocolVersion;
108
+ return successResponse(body.id, {
109
+ protocolVersion: typeof requestedVersion === 'string' && LEGACY_PROTOCOL_VERSIONS.has(requestedVersion)
110
+ ? requestedVersion
111
+ : LEGACY_PROTOCOL_VERSION,
112
+ capabilities: { tools: { listChanged: false } },
113
+ serverInfo,
114
+ instructions,
115
+ }, false, serverInfo);
116
+ }
117
+ case 'ping':
118
+ return successResponse(body.id, {}, modern, serverInfo);
119
+ case 'tools/list':
120
+ return successResponse(body.id, {
121
+ tools: context.listTools(),
122
+ ...(modern && { ttlMs: 0, cacheScope: 'private' }),
123
+ }, modern, serverInfo);
124
+ case 'tools/call': {
125
+ if (typeof body.params?.name !== 'string') {
126
+ return errorResponse(body.id, -32602, 'Tool name is required.');
127
+ }
128
+ const result = await context.callTool(body.params.name, body.params.arguments);
129
+ return successResponse(body.id, {
130
+ content: [{ type: 'text', text: serializeToolOutput(result.output) }],
131
+ isError: result.isError,
132
+ }, modern, serverInfo);
133
+ }
134
+ default:
135
+ return errorResponse(body.id, -32601, `Method not found: ${body.method}`);
136
+ }
137
+ }
@@ -0,0 +1,20 @@
1
+ import type { PluginsCommonOptions } from 'adminforth';
2
+ export interface McpAuthSecretResourceOptions {
3
+ resourceId: string;
4
+ idField: string;
5
+ nameField: string;
6
+ secretHashField: string;
7
+ userIdField: string;
8
+ createdAtField: string;
9
+ lastUsedAtField: string;
10
+ lastUsedByAgentField: string;
11
+ }
12
+ export interface PluginOptions extends PluginsCommonOptions {
13
+ authSecretResource: McpAuthSecretResourceOptions;
14
+ /** Public origin of this AdminForth installation, without the AdminForth baseUrl path. */
15
+ adminPanelOrigin?: string;
16
+ }
17
+ export interface McpClientInfo {
18
+ client: string;
19
+ ver: string | null;
20
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@adminforth/mcp",
3
+ "version": "1.0.0",
4
+ "description": "Expose AdminForth APIs as an authenticated MCP server",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "homepage": "https://adminforth.dev/docs/tutorial/Plugins/mcp/",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/devforth/adminforth-mcp.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/devforth/adminforth-mcp/issues"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc && rsync -av --exclude 'node_modules' custom dist/",
29
+ "test": "pnpm build && node --test tests/*.test.mjs"
30
+ },
31
+ "keywords": [
32
+ "adminforth",
33
+ "mcp",
34
+ "model-context-protocol",
35
+ "ai-agent",
36
+ "codex",
37
+ "claude",
38
+ "gemini"
39
+ ],
40
+ "author": "DevForth (https://devforth.io)",
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "@types/node": "latest",
44
+ "adminforth": "^3.20.0",
45
+ "semantic-release": "^24.2.1",
46
+ "semantic-release-slack-bot": "^4.0.2",
47
+ "typescript": "^5.7.3"
48
+ },
49
+ "peerDependencies": {
50
+ "adminforth": "^3.20.0"
51
+ },
52
+ "release": {
53
+ "plugins": [
54
+ "@semantic-release/commit-analyzer",
55
+ "@semantic-release/release-notes-generator",
56
+ "@semantic-release/npm",
57
+ "@semantic-release/github",
58
+ [
59
+ "semantic-release-slack-bot",
60
+ {
61
+ "packageName": "@adminforth/mcp",
62
+ "notifyOnSuccess": true,
63
+ "notifyOnFail": true,
64
+ "slackIcon": ":package:",
65
+ "markdownReleaseNotes": true
66
+ }
67
+ ]
68
+ ],
69
+ "branches": [
70
+ "main",
71
+ {
72
+ "name": "next",
73
+ "prerelease": true
74
+ }
75
+ ]
76
+ }
77
+ }