@adminforth/mcp 1.0.4 → 1.1.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/apiTools.d.ts +4 -2
- package/dist/apiTools.js +11 -9
- package/dist/authSecretStore.d.ts +3 -1
- package/dist/authSecretStore.js +4 -1
- package/dist/custom/McpSettings.vue +30 -5
- package/dist/index.d.ts +0 -1
- package/dist/index.js +73 -6
- package/dist/mcpProtocol.d.ts +1 -1
- package/dist/mcpProtocol.js +2 -2
- package/dist/types.d.ts +6 -0
- package/package.json +3 -3
package/dist/apiTools.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export interface McpToolDefinition {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
inputSchema: Record<string, unknown>;
|
|
6
6
|
annotations?: {
|
|
7
|
-
|
|
7
|
+
readOnlyHint?: boolean;
|
|
8
|
+
destructiveHint?: boolean;
|
|
8
9
|
};
|
|
9
10
|
}
|
|
10
11
|
export declare class AdminForthApiTools {
|
|
@@ -12,7 +13,7 @@ export declare class AdminForthApiTools {
|
|
|
12
13
|
private readonly hiddenResourceIds;
|
|
13
14
|
constructor(adminforth: IAdminForth, hiddenResourceIds: ReadonlySet<string>);
|
|
14
15
|
private schemas;
|
|
15
|
-
list(): McpToolDefinition[];
|
|
16
|
+
list(readOnly: boolean): McpToolDefinition[];
|
|
16
17
|
call(params: {
|
|
17
18
|
name: string;
|
|
18
19
|
arguments?: Record<string, unknown>;
|
|
@@ -20,6 +21,7 @@ export declare class AdminForthApiTools {
|
|
|
20
21
|
headers: Record<string, any>;
|
|
21
22
|
requestUrl: string;
|
|
22
23
|
abortSignal: AbortSignal;
|
|
24
|
+
readOnly: boolean;
|
|
23
25
|
}): Promise<{
|
|
24
26
|
output: unknown;
|
|
25
27
|
isError: boolean;
|
package/dist/apiTools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const METHODS_WITHOUT_REQUEST_BODY = new Set(['GET', 'HEAD']);
|
|
2
2
|
function isRegisteredToolSchema(schema) {
|
|
3
|
-
return typeof schema.handler === 'function';
|
|
3
|
+
return typeof schema.handler === 'function' && !schema.agent?.hiddenFromAgents;
|
|
4
4
|
}
|
|
5
5
|
function endpointPathToToolName(path) {
|
|
6
6
|
return path
|
|
@@ -34,20 +34,19 @@ export class AdminForthApiTools {
|
|
|
34
34
|
this.adminforth = adminforth;
|
|
35
35
|
this.hiddenResourceIds = hiddenResourceIds;
|
|
36
36
|
}
|
|
37
|
-
schemas() {
|
|
37
|
+
schemas(readOnly) {
|
|
38
38
|
const schemas = new Map();
|
|
39
39
|
for (const schema of this.adminforth.openApi.registeredSchemas) {
|
|
40
40
|
if (!isRegisteredToolSchema(schema))
|
|
41
41
|
continue;
|
|
42
|
-
|
|
43
|
-
if (path === '/mcp' || path.startsWith('/mcp/'))
|
|
42
|
+
if (readOnly && !schema.agent?.onlyReadsData)
|
|
44
43
|
continue;
|
|
45
|
-
schemas.set(endpointPathToToolName(path), schema);
|
|
44
|
+
schemas.set(endpointPathToToolName(stripAdminApiPrefix(schema.path, this.adminforth)), schema);
|
|
46
45
|
}
|
|
47
46
|
return schemas;
|
|
48
47
|
}
|
|
49
|
-
list() {
|
|
50
|
-
return Array.from(this.schemas().entries())
|
|
48
|
+
list(readOnly) {
|
|
49
|
+
return Array.from(this.schemas(readOnly).entries())
|
|
51
50
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
52
51
|
.map(([name, schema]) => ({
|
|
53
52
|
name,
|
|
@@ -57,13 +56,16 @@ export class AdminForthApiTools {
|
|
|
57
56
|
properties: {},
|
|
58
57
|
additionalProperties: true,
|
|
59
58
|
},
|
|
60
|
-
...(schema.agent?.
|
|
59
|
+
...(schema.agent?.onlyReadsData && {
|
|
60
|
+
annotations: { readOnlyHint: true },
|
|
61
|
+
}),
|
|
62
|
+
...(schema.agent?.requiresHumanApproval && {
|
|
61
63
|
annotations: { destructiveHint: true },
|
|
62
64
|
}),
|
|
63
65
|
}));
|
|
64
66
|
}
|
|
65
67
|
async call(params) {
|
|
66
|
-
const schema = this.schemas().get(params.name);
|
|
68
|
+
const schema = this.schemas(params.readOnly).get(params.name);
|
|
67
69
|
if (!schema) {
|
|
68
70
|
return { output: { error: `Unknown tool: ${params.name}` }, isError: true };
|
|
69
71
|
}
|
|
@@ -7,6 +7,7 @@ export interface AuthenticatedMcpSecret {
|
|
|
7
7
|
adminUser: AdminUserWithExecutor;
|
|
8
8
|
client: McpClientInfo | null;
|
|
9
9
|
name: string;
|
|
10
|
+
readOnly: boolean;
|
|
10
11
|
recordId: string;
|
|
11
12
|
}
|
|
12
13
|
export declare class McpAuthSecretStore {
|
|
@@ -20,8 +21,9 @@ export declare class McpAuthSecretStore {
|
|
|
20
21
|
createdAt: any;
|
|
21
22
|
lastUsedAt: any;
|
|
22
23
|
lastUsedByAgent: McpClientInfo | null;
|
|
24
|
+
readOnly: any;
|
|
23
25
|
}[]>;
|
|
24
|
-
create(name: string, adminUser: AdminUser, extra: HttpExtra): Promise<{
|
|
26
|
+
create(name: string, readOnly: boolean, adminUser: AdminUser, extra: HttpExtra): Promise<{
|
|
25
27
|
error: string;
|
|
26
28
|
secret?: undefined;
|
|
27
29
|
} | {
|
package/dist/authSecretStore.js
CHANGED
|
@@ -42,9 +42,10 @@ export class McpAuthSecretStore {
|
|
|
42
42
|
createdAt: record[fields.createdAtField],
|
|
43
43
|
lastUsedAt: record[fields.lastUsedAtField] ?? null,
|
|
44
44
|
lastUsedByAgent: parseStoredClient(record[fields.lastUsedByAgentField]),
|
|
45
|
+
readOnly: record[fields.readOnlyField],
|
|
45
46
|
}));
|
|
46
47
|
}
|
|
47
|
-
async create(name, adminUser, extra) {
|
|
48
|
+
async create(name, readOnly, adminUser, extra) {
|
|
48
49
|
const fields = this.options;
|
|
49
50
|
const secret = `${SECRET_PREFIX}${randomBytes(32).toString('base64url')}`;
|
|
50
51
|
const result = await this.adminforth.createResourceRecord({
|
|
@@ -59,6 +60,7 @@ export class McpAuthSecretStore {
|
|
|
59
60
|
[fields.createdAtField]: new Date().toISOString(),
|
|
60
61
|
[fields.lastUsedAtField]: null,
|
|
61
62
|
[fields.lastUsedByAgentField]: null,
|
|
63
|
+
[fields.readOnlyField]: readOnly,
|
|
62
64
|
},
|
|
63
65
|
});
|
|
64
66
|
return result.error ? { error: result.error } : { secret };
|
|
@@ -95,6 +97,7 @@ export class McpAuthSecretStore {
|
|
|
95
97
|
},
|
|
96
98
|
client: parseStoredClient(record[fields.lastUsedByAgentField]),
|
|
97
99
|
name: record[fields.nameField],
|
|
100
|
+
readOnly: record[fields.readOnlyField],
|
|
98
101
|
recordId: record[fields.idField],
|
|
99
102
|
};
|
|
100
103
|
}
|
|
@@ -35,7 +35,15 @@
|
|
|
35
35
|
:pageSize="10"
|
|
36
36
|
>
|
|
37
37
|
<template #cell:name="{ item }">
|
|
38
|
-
<p class="font-medium text-gray-900 dark:text-white">
|
|
38
|
+
<p class="font-medium text-gray-900 dark:text-white">
|
|
39
|
+
{{ item.name }}
|
|
40
|
+
<span
|
|
41
|
+
v-if="serverReadOnly || item.readOnly"
|
|
42
|
+
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"
|
|
43
|
+
>
|
|
44
|
+
{{ $t('Read only') }}
|
|
45
|
+
</span>
|
|
46
|
+
</p>
|
|
39
47
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ formatDateTime(item.createdAt) }}</p>
|
|
40
48
|
</template>
|
|
41
49
|
|
|
@@ -78,11 +86,19 @@
|
|
|
78
86
|
<div class="mt-2">
|
|
79
87
|
<Input v-model="secretName" type="text" fullWidth placeholder="Claude Code" />
|
|
80
88
|
</div>
|
|
89
|
+
<Checkbox v-if="!serverReadOnly" v-model="secretReadOnly" class="mt-4">
|
|
90
|
+
{{ $t('Read only: the agent can only read data') }}
|
|
91
|
+
</Checkbox>
|
|
81
92
|
</div>
|
|
82
93
|
<div v-else>
|
|
83
94
|
<p class="text-sm text-amber-700 dark:text-amber-300">{{ $t('Copy it now. You will not be able to view it again.') }}</p>
|
|
84
95
|
<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">
|
|
85
|
-
|
|
96
|
+
<template v-if="createdSecretReadOnly">
|
|
97
|
+
{{ $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.') }}
|
|
98
|
+
</template>
|
|
99
|
+
<template v-else>
|
|
100
|
+
{{ $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.') }}
|
|
101
|
+
</template>
|
|
86
102
|
</p>
|
|
87
103
|
<code class="mt-4 block overflow-x-auto rounded-default bg-gray-100 p-3 text-sm text-gray-900 dark:bg-gray-900 dark:text-gray-100">{{ createdSecret }}</code>
|
|
88
104
|
<p class="mt-5 text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Prompt for your agent') }}</p>
|
|
@@ -95,7 +111,7 @@
|
|
|
95
111
|
<script setup lang="ts">
|
|
96
112
|
import { computed, onMounted, ref } from 'vue';
|
|
97
113
|
import { useI18n } from 'vue-i18n';
|
|
98
|
-
import { Button, Dialog, Input, Table } from '@/afcl';
|
|
114
|
+
import { Button, Checkbox, Dialog, Input, Table } from '@/afcl';
|
|
99
115
|
import adminforth from '@/adminforth';
|
|
100
116
|
import { callAdminForthApi, formatDateTime } from '@/utils';
|
|
101
117
|
import claudeCodeIcon from './icons/claude-code.svg';
|
|
@@ -109,6 +125,7 @@ type AuthSecret = {
|
|
|
109
125
|
createdAt: string;
|
|
110
126
|
lastUsedAt: string | null;
|
|
111
127
|
lastUsedByAgent: Agent | null;
|
|
128
|
+
readOnly: boolean;
|
|
112
129
|
};
|
|
113
130
|
|
|
114
131
|
const { t } = useI18n();
|
|
@@ -117,6 +134,8 @@ const authSecrets = ref<AuthSecret[]>([]);
|
|
|
117
134
|
const loading = ref(true);
|
|
118
135
|
const dialogRef = ref();
|
|
119
136
|
const secretName = ref('');
|
|
137
|
+
const secretReadOnly = ref(false);
|
|
138
|
+
const serverReadOnly = ref(false);
|
|
120
139
|
const createdSecret = ref('');
|
|
121
140
|
const creating = ref(false);
|
|
122
141
|
const revokingId = ref<string | null>(null);
|
|
@@ -154,6 +173,8 @@ const dialogButtons = computed(() => createdSecret.value
|
|
|
154
173
|
]
|
|
155
174
|
);
|
|
156
175
|
|
|
176
|
+
const createdSecretReadOnly = computed(() => serverReadOnly.value || secretReadOnly.value);
|
|
177
|
+
|
|
157
178
|
const mcpUrl = computed(() => {
|
|
158
179
|
const baseUrl = (import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || '').replace(/\/$/, '');
|
|
159
180
|
return `${window.location.origin}${baseUrl}/adminapi/v1/mcp`;
|
|
@@ -171,7 +192,10 @@ async function loadAuthSecrets() {
|
|
|
171
192
|
loading.value = true;
|
|
172
193
|
try {
|
|
173
194
|
const response = await callAdminForthApi({ method: 'GET', path: '/mcp/auth-secrets' });
|
|
174
|
-
if (response)
|
|
195
|
+
if (response) {
|
|
196
|
+
authSecrets.value = response.authSecrets;
|
|
197
|
+
serverReadOnly.value = response.serverReadOnly;
|
|
198
|
+
}
|
|
175
199
|
} finally {
|
|
176
200
|
loading.value = false;
|
|
177
201
|
}
|
|
@@ -179,6 +203,7 @@ async function loadAuthSecrets() {
|
|
|
179
203
|
|
|
180
204
|
function openCreateDialog() {
|
|
181
205
|
secretName.value = '';
|
|
206
|
+
secretReadOnly.value = false;
|
|
182
207
|
createdSecret.value = '';
|
|
183
208
|
dialogRef.value?.open();
|
|
184
209
|
}
|
|
@@ -190,7 +215,7 @@ async function createAuthSecret() {
|
|
|
190
215
|
const response = await callAdminForthApi({
|
|
191
216
|
method: 'POST',
|
|
192
217
|
path: '/mcp/auth-secrets',
|
|
193
|
-
body: { name: secretName.value.trim() },
|
|
218
|
+
body: { name: secretName.value.trim(), readOnly: secretReadOnly.value },
|
|
194
219
|
});
|
|
195
220
|
if (response?.secret) {
|
|
196
221
|
createdSecret.value = response.secret;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
5
5
|
pluginsScope: 'global';
|
|
6
6
|
private authSecretStore;
|
|
7
7
|
private apiTools;
|
|
8
|
-
private serverPresentation;
|
|
9
8
|
constructor(options: PluginOptions);
|
|
10
9
|
instanceUniqueRepresentation(): string;
|
|
11
10
|
modifyGlobalConfig(adminforth: IAdminForth): void;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { AdminForthPlugin, } from 'adminforth';
|
|
2
2
|
import { AdminForthApiTools } from './apiTools.js';
|
|
3
3
|
import { formatMcpExecutedBy, readMcpClient, UNKNOWN_CLIENT } from './clientInfo.js';
|
|
4
|
-
import { createMcpServerPresentation, handleMcpProtocol
|
|
4
|
+
import { createMcpServerPresentation, handleMcpProtocol } from './mcpProtocol.js';
|
|
5
5
|
import { McpAuthSecretStore } from './authSecretStore.js';
|
|
6
6
|
const BEARER_SECRET_RE = /^Bearer (afmcp_[A-Za-z0-9_-]+)$/i;
|
|
7
7
|
const MCP_PATH = '/mcp';
|
|
8
8
|
const CREDENTIAL_HEADERS = new Set(['authorization', 'proxy-authorization', 'cookie']);
|
|
9
|
+
const RESOURCES_LIST_RESPONSE_SCHEMA = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
required: ['resources'],
|
|
12
|
+
properties: {
|
|
13
|
+
resources: {
|
|
14
|
+
type: 'array',
|
|
15
|
+
description: 'Resources the authenticated admin user can access. A resource is not listed when list, show, create, edit and delete are all forbidden for the user.',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
required: ['resourceId', 'label'],
|
|
19
|
+
properties: {
|
|
20
|
+
resourceId: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Resource identifier. Pass it as resourceId to get_resource, get_resource_data, aggregate, create_record, update_record, delete_record and other resource tools. Call get_resource with it to get the columns and allowed actions of the resource.',
|
|
23
|
+
},
|
|
24
|
+
label: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Human readable resource name, translated for the current user.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
9
33
|
function requestExtra(input) {
|
|
10
34
|
return {
|
|
11
35
|
body: input.body,
|
|
@@ -35,7 +59,6 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
35
59
|
pluginsScope = 'global';
|
|
36
60
|
authSecretStore;
|
|
37
61
|
apiTools;
|
|
38
|
-
serverPresentation;
|
|
39
62
|
constructor(options) {
|
|
40
63
|
super(options, import.meta.url);
|
|
41
64
|
this.options = options;
|
|
@@ -73,7 +96,6 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
73
96
|
}
|
|
74
97
|
this.authSecretStore = new McpAuthSecretStore(adminforth, this.options.authSecretResource);
|
|
75
98
|
this.apiTools = new AdminForthApiTools(adminforth, new Set([this.options.authSecretResource.resourceId]));
|
|
76
|
-
this.serverPresentation = createMcpServerPresentation(adminforth.config.customization.brandName, this.options.adminPanelOrigin, adminforth.config.baseUrl);
|
|
77
99
|
}
|
|
78
100
|
validateAuthSecretResource(resource) {
|
|
79
101
|
const fields = this.options.authSecretResource;
|
|
@@ -85,6 +107,7 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
85
107
|
fields.createdAtField,
|
|
86
108
|
fields.lastUsedAtField,
|
|
87
109
|
fields.lastUsedByAgentField,
|
|
110
|
+
fields.readOnlyField,
|
|
88
111
|
]) {
|
|
89
112
|
if (!resource.columns.some((column) => column.name === fieldName)) {
|
|
90
113
|
throw new Error(`AdminForthMcpPlugin: column "${fieldName}" not found in auth secret resource "${resource.resourceId}"`);
|
|
@@ -95,28 +118,54 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
setupEndpoints(server) {
|
|
121
|
+
server.endpoint({
|
|
122
|
+
method: 'GET',
|
|
123
|
+
path: '/get_resources_list',
|
|
124
|
+
description: 'Lists the resourceId and label of every resource (data table). Call this first to discover valid resourceId values before using get_resource, get_resource_data, aggregate, create_record, update_record, delete_record or other resource tools.',
|
|
125
|
+
agent: {
|
|
126
|
+
onlyReadsData: true,
|
|
127
|
+
},
|
|
128
|
+
response_schema: RESOURCES_LIST_RESPONSE_SCHEMA,
|
|
129
|
+
handler: async ({ tr }) => {
|
|
130
|
+
const resources = await Promise.all(this.adminforth.config.resources.map(async (resource) => ({
|
|
131
|
+
resourceId: resource.resourceId,
|
|
132
|
+
label: await tr(resource.label, `resource.${resource.resourceId}`),
|
|
133
|
+
})));
|
|
134
|
+
return { resources };
|
|
135
|
+
},
|
|
136
|
+
});
|
|
98
137
|
server.endpoint({
|
|
99
138
|
method: 'GET',
|
|
100
139
|
path: `${MCP_PATH}/auth-secrets`,
|
|
140
|
+
agent: {
|
|
141
|
+
hiddenFromAgents: true,
|
|
142
|
+
},
|
|
101
143
|
handler: async ({ adminUser }) => ({
|
|
102
144
|
authSecrets: await this.authSecretStore.list(adminUser),
|
|
145
|
+
serverReadOnly: this.options.readOnly ?? false,
|
|
103
146
|
}),
|
|
104
147
|
});
|
|
105
148
|
server.endpoint({
|
|
106
149
|
method: 'POST',
|
|
107
150
|
path: `${MCP_PATH}/auth-secrets`,
|
|
151
|
+
agent: {
|
|
152
|
+
hiddenFromAgents: true,
|
|
153
|
+
},
|
|
108
154
|
handler: async (input) => {
|
|
109
155
|
const name = String(input.body.name ?? '').trim();
|
|
110
156
|
if (!name) {
|
|
111
157
|
input.response.setStatus(400);
|
|
112
158
|
return { error: 'Auth secret name is required' };
|
|
113
159
|
}
|
|
114
|
-
return this.authSecretStore.create(name, input.adminUser, requestExtra(input));
|
|
160
|
+
return this.authSecretStore.create(name, this.options.readOnly || input.body.readOnly === true, input.adminUser, requestExtra(input));
|
|
115
161
|
},
|
|
116
162
|
});
|
|
117
163
|
server.endpoint({
|
|
118
164
|
method: 'DELETE',
|
|
119
165
|
path: `${MCP_PATH}/auth-secrets`,
|
|
166
|
+
agent: {
|
|
167
|
+
hiddenFromAgents: true,
|
|
168
|
+
},
|
|
120
169
|
handler: async (input) => {
|
|
121
170
|
const result = await this.authSecretStore.revoke(String(input.body.id), input.adminUser, requestExtra(input));
|
|
122
171
|
if (result.error)
|
|
@@ -127,9 +176,25 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
127
176
|
server.endpoint({
|
|
128
177
|
method: 'POST',
|
|
129
178
|
path: MCP_PATH,
|
|
179
|
+
agent: {
|
|
180
|
+
hiddenFromAgents: true,
|
|
181
|
+
},
|
|
130
182
|
noAuth: true,
|
|
131
183
|
handler: async (input) => this.handleMcpRequest(input),
|
|
132
184
|
});
|
|
185
|
+
// Streamable HTTP clients open a GET stream for server-initiated messages; 405 tells them this server has none.
|
|
186
|
+
server.endpoint({
|
|
187
|
+
method: 'GET',
|
|
188
|
+
path: MCP_PATH,
|
|
189
|
+
agent: {
|
|
190
|
+
hiddenFromAgents: true,
|
|
191
|
+
},
|
|
192
|
+
noAuth: true,
|
|
193
|
+
handler: async ({ response }) => {
|
|
194
|
+
response.setHeader('Allow', 'POST');
|
|
195
|
+
response.setStatus(405, 'Method Not Allowed');
|
|
196
|
+
},
|
|
197
|
+
});
|
|
133
198
|
}
|
|
134
199
|
async handleMcpRequest(input) {
|
|
135
200
|
if (input.headers.origin) {
|
|
@@ -164,11 +229,12 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
164
229
|
...authenticated.adminUser,
|
|
165
230
|
executedBy: formatMcpExecutedBy(client, authenticated.name),
|
|
166
231
|
};
|
|
232
|
+
const readOnly = this.options.readOnly || authenticated.readOnly;
|
|
167
233
|
const protocolResponse = await handleMcpProtocol({
|
|
168
|
-
...this.
|
|
234
|
+
...createMcpServerPresentation(this.adminforth.config.customization.brandName, this.options.adminPanelOrigin, this.adminforth.config.baseUrl, readOnly),
|
|
169
235
|
body: input.body,
|
|
170
236
|
headers: toolHeaders,
|
|
171
|
-
listTools: () => this.apiTools.list(),
|
|
237
|
+
listTools: () => this.apiTools.list(readOnly),
|
|
172
238
|
callTool: (name, arguments_) => this.apiTools.call({
|
|
173
239
|
name,
|
|
174
240
|
arguments: arguments_,
|
|
@@ -176,6 +242,7 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
|
|
|
176
242
|
headers: toolHeaders,
|
|
177
243
|
requestUrl: input.requestUrl,
|
|
178
244
|
abortSignal: input.abortSignal,
|
|
245
|
+
readOnly,
|
|
179
246
|
}),
|
|
180
247
|
});
|
|
181
248
|
input.response.setStatus(protocolResponse.status);
|
package/dist/mcpProtocol.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface McpServerPresentation {
|
|
|
12
12
|
serverInfo: McpServerInfo;
|
|
13
13
|
instructions: string;
|
|
14
14
|
}
|
|
15
|
-
export declare function createMcpServerPresentation(brandName: string, adminPanelOrigin?: string, baseUrl?: string): McpServerPresentation;
|
|
15
|
+
export declare function createMcpServerPresentation(brandName: string, adminPanelOrigin?: string, baseUrl?: string, readOnly?: boolean): McpServerPresentation;
|
|
16
16
|
export interface McpProtocolContext {
|
|
17
17
|
body: Record<string, any>;
|
|
18
18
|
headers: Record<string, any>;
|
package/dist/mcpProtocol.js
CHANGED
|
@@ -8,7 +8,7 @@ const LEGACY_PROTOCOL_VERSIONS = new Set([
|
|
|
8
8
|
const PROTOCOL_VERSION_META_KEY = 'io.modelcontextprotocol/protocolVersion';
|
|
9
9
|
const CLIENT_CAPABILITIES_META_KEY = 'io.modelcontextprotocol/clientCapabilities';
|
|
10
10
|
const SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo';
|
|
11
|
-
export function createMcpServerPresentation(brandName, adminPanelOrigin, baseUrl = '') {
|
|
11
|
+
export function createMcpServerPresentation(brandName, adminPanelOrigin, baseUrl = '', readOnly = false) {
|
|
12
12
|
const adminPanelUrl = adminPanelOrigin
|
|
13
13
|
? new URL(baseUrl || '/', adminPanelOrigin).toString()
|
|
14
14
|
: undefined;
|
|
@@ -21,7 +21,7 @@ export function createMcpServerPresentation(brandName, adminPanelOrigin, baseUrl
|
|
|
21
21
|
description: `${adminPanel}.`,
|
|
22
22
|
...(adminPanelUrl && { websiteUrl: adminPanelUrl }),
|
|
23
23
|
},
|
|
24
|
-
instructions: `This is the ${adminPanel}. Use its tools to read and update data allowed for the authenticated user.`,
|
|
24
|
+
instructions: `This is the ${adminPanel}. Use its tools to ${readOnly ? 'read' : 'read and update'} data allowed for the authenticated user.`,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
function errorResponse(id, code, message, status = 400) {
|
package/dist/types.d.ts
CHANGED
|
@@ -8,11 +8,17 @@ export interface McpAuthSecretResourceOptions {
|
|
|
8
8
|
createdAtField: string;
|
|
9
9
|
lastUsedAtField: string;
|
|
10
10
|
lastUsedByAgentField: string;
|
|
11
|
+
readOnlyField: string;
|
|
11
12
|
}
|
|
12
13
|
export interface PluginOptions extends PluginsCommonOptions {
|
|
13
14
|
authSecretResource: McpAuthSecretResourceOptions;
|
|
14
15
|
/** Public origin of this AdminForth installation, without the AdminForth baseUrl path. */
|
|
15
16
|
adminPanelOrigin?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Expose only endpoints marked with `agent: { onlyReadsData: true }` to every auth secret.
|
|
19
|
+
* Without it, read-only mode is chosen per auth secret.
|
|
20
|
+
*/
|
|
21
|
+
readOnly?: boolean;
|
|
16
22
|
}
|
|
17
23
|
export interface McpClientInfo {
|
|
18
24
|
client: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "latest",
|
|
44
|
-
"adminforth": "^3.
|
|
44
|
+
"adminforth": "^3.32.0",
|
|
45
45
|
"semantic-release": "^24.2.1",
|
|
46
46
|
"semantic-release-slack-bot": "^4.0.2",
|
|
47
47
|
"typescript": "^5.7.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"adminforth": "^3.
|
|
50
|
+
"adminforth": "^3.32.0"
|
|
51
51
|
},
|
|
52
52
|
"release": {
|
|
53
53
|
"plugins": [
|