@adminforth/mcp 1.0.3 → 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.
@@ -4,7 +4,8 @@ export interface McpToolDefinition {
4
4
  description?: string;
5
5
  inputSchema: Record<string, unknown>;
6
6
  annotations?: {
7
- destructiveHint: boolean;
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
- const path = stripAdminApiPrefix(schema.path, this.adminforth);
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?.isDangerous && {
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
  } | {
@@ -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,8 +35,16 @@
35
35
  :pageSize="10"
36
36
  >
37
37
  <template #cell:name="{ item }">
38
- <p class="font-medium text-gray-900 dark:text-white">{{ item.name }}</p>
39
- <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ formatDate(item.createdAt) }}</p>
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>
47
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{{ formatDateTime(item.createdAt) }}</p>
40
48
  </template>
41
49
 
42
50
  <template #cell:lastUsedByAgent="{ item }">
@@ -48,7 +56,7 @@
48
56
  </template>
49
57
 
50
58
  <template #cell:lastUsedAt="{ item }">
51
- {{ item.lastUsedAt ? formatDate(item.lastUsedAt) : $t('Never') }}
59
+ {{ item.lastUsedAt ? formatDateTime(item.lastUsedAt) : $t('Never') }}
52
60
  </template>
53
61
 
54
62
  <template #cell:actions="{ item }">
@@ -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
- {{ $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.') }}
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,9 +111,9 @@
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
- import { callAdminForthApi } from '@/utils';
116
+ import { callAdminForthApi, formatDateTime } from '@/utils';
101
117
  import claudeCodeIcon from './icons/claude-code.svg';
102
118
  import codexIcon from './icons/codex.svg';
103
119
  import geminiIcon from './icons/gemini.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) authSecrets.value = response.authSecrets;
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;
@@ -238,10 +263,6 @@ function describeAgent(agent: Agent) {
238
263
  return { label: agent.ver ? `${name} v${agent.ver}` : name, icon };
239
264
  }
240
265
 
241
- function formatDate(value: string) {
242
- return new Date(value).toLocaleString();
243
- }
244
-
245
266
  async function copy(value: string, message: string) {
246
267
  await navigator.clipboard.writeText(value);
247
268
  adminforth.alert({ message, variant: 'success' });
@@ -1 +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>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><linearGradient id="gemini" x1="2" y1="22" x2="22" y2="2" gradientUnits="userSpaceOnUse"><stop stop-color="#4285F4"/><stop offset=".52" stop-color="#9B72CB"/><stop offset="1" stop-color="#D96570"/></linearGradient></defs><path fill="url(#gemini)" d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
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, } from './mcpProtocol.js';
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.serverPresentation,
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);
@@ -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>;
@@ -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",
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.20.0",
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.20.0"
50
+ "adminforth": "^3.32.0"
51
51
  },
52
52
  "release": {
53
53
  "plugins": [