@adminforth/mcp 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
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
6
 
7
+ > **Security:** an MCP auth secret carries the full permissions of the user who created it. An agent
8
+ > holding it can do everything that user can do in the admin panel, including destructive actions and
9
+ > changes to that user's security settings. Issue one secret per agent, treat it like a password, and
10
+ > revoke it as soon as the agent or its machine is compromised.
11
+
7
12
  Install with:
8
13
 
9
14
  ```bash
@@ -19,7 +19,7 @@ export declare class McpAuthSecretStore {
19
19
  name: any;
20
20
  createdAt: any;
21
21
  lastUsedAt: any;
22
- lastUsedByAgent: any;
22
+ lastUsedByAgent: McpClientInfo | null;
23
23
  }[]>;
24
24
  create(name: string, adminUser: AdminUser, extra: HttpExtra): Promise<{
25
25
  error: string;
@@ -5,6 +5,25 @@ const SECRET_PREFIX = 'afmcp_';
5
5
  function hashSecret(secret) {
6
6
  return createHash('sha256').update(secret).digest('hex');
7
7
  }
8
+ /**
9
+ * Stored agent info originates from the MCP client, so it is never trusted to be valid JSON:
10
+ * a database column which truncated an oversized value would otherwise break every next
11
+ * request made with the auth secret, including the page used to revoke it.
12
+ */
13
+ function parseStoredClient(value) {
14
+ if (typeof value !== 'string' || !value)
15
+ return null;
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(value);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.client !== 'string')
24
+ return null;
25
+ return { client: parsed.client, ver: typeof parsed.ver === 'string' ? parsed.ver : null };
26
+ }
8
27
  export class McpAuthSecretStore {
9
28
  adminforth;
10
29
  options;
@@ -22,9 +41,7 @@ export class McpAuthSecretStore {
22
41
  name: record[fields.nameField],
23
42
  createdAt: record[fields.createdAtField],
24
43
  lastUsedAt: record[fields.lastUsedAtField] ?? null,
25
- lastUsedByAgent: record[fields.lastUsedByAgentField]
26
- ? JSON.parse(record[fields.lastUsedByAgentField])
27
- : null,
44
+ lastUsedByAgent: parseStoredClient(record[fields.lastUsedByAgentField]),
28
45
  }));
29
46
  }
30
47
  async create(name, adminUser, extra) {
@@ -76,9 +93,7 @@ export class McpAuthSecretStore {
76
93
  username: dbUser[auth.usernameField],
77
94
  dbUser,
78
95
  },
79
- client: record[fields.lastUsedByAgentField]
80
- ? JSON.parse(record[fields.lastUsedByAgentField])
81
- : null,
96
+ client: parseStoredClient(record[fields.lastUsedByAgentField]),
82
97
  name: record[fields.nameField],
83
98
  recordId: record[fields.idField],
84
99
  };
@@ -1,22 +1,39 @@
1
1
  const CLIENT_INFO_META_KEY = 'io.modelcontextprotocol/clientInfo';
2
2
  const NON_CLIENT_NAME_CHARACTER_RE = /[^a-z0-9]+/g;
3
3
  const EDGE_HYPHEN_RE = /^-+|-+$/g;
4
+ const NON_CLIENT_VERSION_CHARACTER_RE = /[^A-Za-z0-9._-]+/g;
4
5
  const USER_AGENT_PRODUCT_RE = /^([^/\s]+)(?:\/(\S+))?/;
6
+ // Client name and version are attacker-controlled: they are persisted as JSON and rendered into
7
+ // the audit "executed by" string, so they are sanitized and capped here, at the single entry point.
8
+ const MAX_CLIENT_NAME_LENGTH = 64;
9
+ const MAX_CLIENT_VERSION_LENGTH = 32;
5
10
  export const UNKNOWN_CLIENT = 'unknown-agent';
6
11
  function normalizeClientName(name) {
7
- return name.toLowerCase().replace(NON_CLIENT_NAME_CHARACTER_RE, '-').replace(EDGE_HYPHEN_RE, '');
12
+ if (typeof name !== 'string')
13
+ return '';
14
+ return name
15
+ .toLowerCase()
16
+ .replace(NON_CLIENT_NAME_CHARACTER_RE, '-')
17
+ .slice(0, MAX_CLIENT_NAME_LENGTH)
18
+ .replace(EDGE_HYPHEN_RE, '');
19
+ }
20
+ function normalizeClientVersion(version) {
21
+ if (typeof version !== 'string')
22
+ return null;
23
+ return version
24
+ .replace(NON_CLIENT_VERSION_CHARACTER_RE, '')
25
+ .slice(0, MAX_CLIENT_VERSION_LENGTH) || null;
8
26
  }
9
27
  function parseUserAgent(userAgent) {
10
28
  const [, name, version] = userAgent.match(USER_AGENT_PRODUCT_RE) ?? [];
11
- return name ? { client: normalizeClientName(name), ver: version ?? null } : null;
29
+ const client = normalizeClientName(name);
30
+ return client ? { client, ver: normalizeClientVersion(version) } : null;
12
31
  }
13
32
  export function readMcpClient(body, headers) {
14
33
  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
- };
34
+ const client = normalizeClientName(clientInfo?.name);
35
+ if (client) {
36
+ return { client, ver: normalizeClientVersion(clientInfo.version) };
20
37
  }
21
38
  return parseUserAgent(String(headers['user-agent'] ?? '')) ?? {
22
39
  client: UNKNOWN_CLIENT,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex flex-col justify-center mr-6 md:mr-12">
2
+ <div class="af-mcp-settings flex flex-col justify-center mr-6 md:mr-12">
3
3
  <div class="flex flex-wrap items-start justify-between gap-4">
4
4
  <div>
5
5
  <h2 class="text-3xl font-semibold leading-none text-gray-800 dark:text-gray-50">
@@ -9,113 +9,94 @@
9
9
  {{ $t('Create a separate auth secret for each AI agent. Secrets are shown only once.') }}
10
10
  </p>
11
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
- >
12
+ <Button @click="openCreateDialog">
17
13
  {{ $t('Create auth secret') }}
18
- </button>
14
+ </Button>
19
15
  </div>
20
16
 
21
- <div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
17
+ <div class="mt-6 rounded-default border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
22
18
  <p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('MCP server URL') }}</p>
23
19
  <div class="mt-2 flex items-center gap-2">
24
20
  <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)">
21
+ <Button variant="secondary" @click="copy(mcpUrl, $t('MCP server URL copied'))">
26
22
  {{ $t('Copy') }}
27
- </button>
23
+ </Button>
28
24
  </div>
29
25
  <p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
30
26
  <code>Authorization: Bearer &lt;secret&gt;</code>
31
27
  </p>
32
28
  </div>
33
29
 
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>
30
+ <Table
31
+ class="mt-6"
32
+ :columns="columns"
33
+ :data="authSecrets"
34
+ :isLoading="loading"
35
+ :pageSize="10"
36
+ >
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>
40
+ </template>
41
+
42
+ <template #cell:lastUsedByAgent="{ item }">
43
+ <div v-if="item.lastUsedByAgent" class="flex items-center gap-2">
44
+ <img v-if="describeAgent(item.lastUsedByAgent).icon" :src="describeAgent(item.lastUsedByAgent).icon" class="h-5 w-5" alt="" />
45
+ <span>{{ describeAgent(item.lastUsedByAgent).label }}</span>
46
+ </div>
47
+ <span v-else class="text-gray-400">{{ $t('Not used yet') }}</span>
48
+ </template>
49
+
50
+ <template #cell:lastUsedAt="{ item }">
51
+ {{ item.lastUsedAt ? formatDate(item.lastUsedAt) : $t('Never') }}
52
+ </template>
53
+
54
+ <template #cell:actions="{ item }">
55
+ <div class="flex justify-end">
56
+ <Button
57
+ variant="danger"
58
+ :loader="revokingId === item.id"
59
+ :disabled="revokingId === item.id"
60
+ @click="revoke(item)"
61
+ >
62
+ {{ revokingId === item.id ? $t('Revoking') : $t('Revoke') }}
63
+ </Button>
64
+ </div>
65
+ </template>
66
+ </Table>
67
+
68
+ <Dialog
69
+ ref="dialogRef"
70
+ class="w-full max-w-xl"
71
+ :header="createdSecret ? $t('Auth secret created') : $t('Create MCP auth secret')"
72
+ :buttons="dialogButtons"
73
+ >
74
+ <!-- enter is handled on the wrapper so it fires once: Input spreads attrs on both its root and the input -->
75
+ <div v-if="!createdSecret" @keydown.enter="createAuthSecret">
76
+ <p class="text-sm text-gray-500 dark:text-gray-400">{{ $t('Use one auth secret for one agent.') }}</p>
77
+ <label class="mt-5 block text-sm font-medium text-gray-700 dark:text-gray-200">{{ $t('Secret name') }}</label>
78
+ <div class="mt-2">
79
+ <Input v-model="secretName" type="text" fullWidth placeholder="Claude Code" />
80
+ </div>
112
81
  </div>
113
- </div>
82
+ <div v-else>
83
+ <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
+ <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.') }}
86
+ </p>
87
+ <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
+ <p class="mt-5 text-xs font-medium uppercase text-gray-500 dark:text-gray-400">{{ $t('Prompt for your agent') }}</p>
89
+ <pre class="mt-2 whitespace-pre-wrap rounded-default bg-gray-100 p-3 text-sm text-gray-800 dark:bg-gray-900 dark:text-gray-100">{{ setupPrompt }}</pre>
90
+ </div>
91
+ </Dialog>
114
92
  </div>
115
93
  </template>
116
94
 
117
95
  <script setup lang="ts">
118
96
  import { computed, onMounted, ref } from 'vue';
97
+ import { useI18n } from 'vue-i18n';
98
+ import { Button, Dialog, Input, Table } from '@/afcl';
99
+ import adminforth from '@/adminforth';
119
100
  import { callAdminForthApi } from '@/utils';
120
101
  import claudeCodeIcon from './icons/claude-code.svg';
121
102
  import codexIcon from './icons/codex.svg';
@@ -130,14 +111,49 @@ type AuthSecret = {
130
111
  lastUsedByAgent: Agent | null;
131
112
  };
132
113
 
114
+ const { t } = useI18n();
115
+
133
116
  const authSecrets = ref<AuthSecret[]>([]);
134
117
  const loading = ref(true);
135
- const dialogOpen = ref(false);
118
+ const dialogRef = ref();
136
119
  const secretName = ref('');
137
120
  const createdSecret = ref('');
138
121
  const creating = ref(false);
139
122
  const revokingId = ref<string | null>(null);
140
123
 
124
+ const columns = computed(() => [
125
+ { label: t('Name'), fieldName: 'name', sortable: true },
126
+ { label: t('Last used by'), fieldName: 'lastUsedByAgent' },
127
+ { label: t('Last usage'), fieldName: 'lastUsedAt', sortable: true },
128
+ { label: t('Actions'), fieldName: 'actions' },
129
+ ]);
130
+
131
+ const dialogButtons = computed(() => createdSecret.value
132
+ ? [
133
+ {
134
+ label: t('Copy prompt'),
135
+ options: { variant: 'secondary' },
136
+ onclick: () => copy(setupPrompt.value, t('Prompt copied')),
137
+ },
138
+ {
139
+ label: t('Done'),
140
+ onclick: (dialog: { hide: () => void }) => dialog.hide(),
141
+ },
142
+ ]
143
+ : [
144
+ {
145
+ label: t('Cancel'),
146
+ options: { variant: 'secondary' },
147
+ onclick: (dialog: { hide: () => void }) => dialog.hide(),
148
+ },
149
+ {
150
+ label: creating.value ? t('Creating') : t('Create'),
151
+ options: { loader: creating.value, disabled: creating.value || !secretName.value.trim() },
152
+ onclick: () => createAuthSecret(),
153
+ },
154
+ ]
155
+ );
156
+
141
157
  const mcpUrl = computed(() => {
142
158
  const baseUrl = (import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || '').replace(/\/$/, '');
143
159
  return `${window.location.origin}${baseUrl}/adminapi/v1/mcp`;
@@ -164,15 +180,11 @@ async function loadAuthSecrets() {
164
180
  function openCreateDialog() {
165
181
  secretName.value = '';
166
182
  createdSecret.value = '';
167
- dialogOpen.value = true;
168
- }
169
-
170
- function closeDialog() {
171
- dialogOpen.value = false;
172
- createdSecret.value = '';
183
+ dialogRef.value?.open();
173
184
  }
174
185
 
175
186
  async function createAuthSecret() {
187
+ if (creating.value || !secretName.value.trim()) return;
176
188
  creating.value = true;
177
189
  try {
178
190
  const response = await callAdminForthApi({
@@ -190,7 +202,13 @@ async function createAuthSecret() {
190
202
  }
191
203
 
192
204
  async function revoke(authSecret: AuthSecret) {
193
- if (!window.confirm(`Revoke MCP auth secret "${authSecret.name}"?`)) return;
205
+ const confirmed = await adminforth.confirm({
206
+ message: t('Revoke MCP auth secret "{name}"? Any agent using it will lose access.', { name: authSecret.name }),
207
+ yes: t('Revoke'),
208
+ no: t('Cancel'),
209
+ dangerous: true,
210
+ });
211
+ if (!confirmed) return;
194
212
  revokingId.value = authSecret.id;
195
213
  try {
196
214
  await callAdminForthApi({ method: 'DELETE', path: '/mcp/auth-secrets', body: { id: authSecret.id } });
@@ -224,7 +242,8 @@ function formatDate(value: string) {
224
242
  return new Date(value).toLocaleString();
225
243
  }
226
244
 
227
- async function copy(value: string) {
245
+ async function copy(value: string, message: string) {
228
246
  await navigator.clipboard.writeText(value);
247
+ adminforth.alert({ message, variant: 'success' });
229
248
  }
230
249
  </script>
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { createMcpServerPresentation, handleMcpProtocol, } from './mcpProtocol.j
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
+ const CREDENTIAL_HEADERS = new Set(['authorization', 'proxy-authorization', 'cookie']);
8
9
  function requestExtra(input) {
9
10
  return {
10
11
  body: input.body,
@@ -15,6 +16,13 @@ function requestExtra(input) {
15
16
  response: input.response,
16
17
  };
17
18
  }
19
+ /**
20
+ * The MCP auth secret is a long-lived credential, so it is dropped as soon as it is verified and
21
+ * never reaches tool handlers, which may log, persist or forward the headers they are given.
22
+ */
23
+ function withoutCredentials(headers) {
24
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => !CREDENTIAL_HEADERS.has(name.toLowerCase())));
25
+ }
18
26
  function jsonRpcAuthError(id = null) {
19
27
  return {
20
28
  jsonrpc: '2.0',
@@ -146,7 +154,8 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
146
154
  input.response.setStatus(403);
147
155
  return jsonRpcAuthError(input.body.id ?? null);
148
156
  }
149
- const reportedClient = readMcpClient(input.body, input.headers);
157
+ const toolHeaders = withoutCredentials(input.headers);
158
+ const reportedClient = readMcpClient(input.body, toolHeaders);
150
159
  const client = reportedClient.client === UNKNOWN_CLIENT && authenticated.client
151
160
  ? authenticated.client
152
161
  : reportedClient;
@@ -158,13 +167,13 @@ export default class AdminForthMcpPlugin extends AdminForthPlugin {
158
167
  const protocolResponse = await handleMcpProtocol({
159
168
  ...this.serverPresentation,
160
169
  body: input.body,
161
- headers: input.headers,
170
+ headers: toolHeaders,
162
171
  listTools: () => this.apiTools.list(),
163
172
  callTool: (name, arguments_) => this.apiTools.call({
164
173
  name,
165
174
  arguments: arguments_,
166
175
  adminUser,
167
- headers: input.headers,
176
+ headers: toolHeaders,
168
177
  requestUrl: input.requestUrl,
169
178
  abortSignal: input.abortSignal,
170
179
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Expose AdminForth APIs as an authenticated MCP server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",