apifm-admin-mcp 26.5.1 → 26.5.2
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 +10 -7
- package/package.json +2 -2
- package/src/index.js +125 -48
- package/src/self-check.js +12 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Secrets must not be pasted into model chat.
|
|
|
10
10
|
|
|
11
11
|
This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`, or username/email password login details in that local browser page. Those secrets stay in the local MCP process memory and are never part of MCP tool arguments.
|
|
12
12
|
|
|
13
|
-
The
|
|
13
|
+
The API callers reject payloads and headers containing sensitive field names such as `pwd`, `password`, `token`, `x-token`, `authorization`, `secret`, or `key`.
|
|
14
14
|
|
|
15
15
|
Accounts are in-memory only. Restarting the MCP server clears all tokens and credentials.
|
|
16
16
|
|
|
@@ -55,17 +55,20 @@ If installed globally:
|
|
|
55
55
|
- `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
|
|
56
56
|
- `apifm_admin_switch_account`: Switches the active account alias.
|
|
57
57
|
- `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
58
|
+
- `apifm_admin_find_and_call`: Searches for the best matching SDK method, calls it, and returns the live backend response in `apiResult`.
|
|
59
|
+
- `apifm_admin_call`: Calls an exact SDK method using the selected local account and returns the live backend response in `apiResult`.
|
|
60
|
+
- `apifm_admin_search_methods`: Planning helper only. Searches methods dynamically from the installed `apifm-admin` SDK, but does not call the API.
|
|
61
|
+
- `apifm_admin_method_info`: Planning helper only. Shows route, method, parameters, and usage for one SDK method, but does not call the API.
|
|
62
|
+
|
|
63
|
+
For real backend data, agents should call `apifm_admin_find_and_call` or `apifm_admin_call`. The final API response is returned under `apiResult`.
|
|
61
64
|
|
|
62
65
|
## Example Agent Flow
|
|
63
66
|
|
|
64
|
-
1. User:
|
|
67
|
+
1. User: "Log in to my admin account and read the user list."
|
|
65
68
|
2. Agent calls `apifm_admin_start_auth` with `authType: "password"`.
|
|
66
69
|
3. User opens the returned local URL and enters username/email and password there.
|
|
67
|
-
4. Agent calls `
|
|
68
|
-
5. Agent
|
|
70
|
+
4. Agent calls `apifm_admin_find_and_call` with `query: "user list"` and a non-sensitive payload such as `{ "page": 1, "pageSize": 20 }`.
|
|
71
|
+
5. Agent answers from the returned `apiResult`, not from method documentation.
|
|
69
72
|
|
|
70
73
|
## Local Check
|
|
71
74
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apifm-admin-mcp",
|
|
3
|
-
"version": "26.5.
|
|
3
|
+
"version": "26.5.2",
|
|
4
4
|
"description": "MCP server for safely operating APIFM admin APIs through the apifm-admin SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"apifm-admin-mcp": "
|
|
7
|
+
"apifm-admin-mcp": "src/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
summarizeMetadata
|
|
22
22
|
} from './sdk.js'
|
|
23
23
|
|
|
24
|
-
const VERSION = '26.5.
|
|
24
|
+
const VERSION = '26.5.2'
|
|
25
25
|
|
|
26
26
|
const server = new McpServer(
|
|
27
27
|
{
|
|
@@ -33,7 +33,8 @@ const server = new McpServer(
|
|
|
33
33
|
'Use this MCP server to operate APIFM admin APIs through the apifm-admin SDK.',
|
|
34
34
|
'Never ask users to paste passwords, Basic Authentication values, X-Token values, or API keys into chat.',
|
|
35
35
|
'Use apifm_admin_start_auth to collect secrets through a local browser page, then call APIs by account alias.',
|
|
36
|
-
'
|
|
36
|
+
'For user requests that ask to read, create, update, delete, or operate backend data, call apifm_admin_find_and_call or apifm_admin_call and return the live apiResult.',
|
|
37
|
+
'apifm_admin_search_methods and apifm_admin_method_info are only planning helpers; do not treat their parameter documentation as the final answer.'
|
|
37
38
|
].join('\n')
|
|
38
39
|
}
|
|
39
40
|
)
|
|
@@ -114,7 +115,7 @@ server.registerTool(
|
|
|
114
115
|
{
|
|
115
116
|
title: 'Search apifm-admin SDK methods',
|
|
116
117
|
description:
|
|
117
|
-
'Searches
|
|
118
|
+
'Planning helper only. Searches SDK methods and parameter docs, but does not call the API. After choosing a method, use apifm_admin_call or apifm_admin_find_and_call to get live API data.',
|
|
118
119
|
inputSchema: z.object({
|
|
119
120
|
query: z.string().optional(),
|
|
120
121
|
limit: z.number().int().min(1).max(100).optional()
|
|
@@ -131,7 +132,8 @@ server.registerTool(
|
|
|
131
132
|
'apifm_admin_method_info',
|
|
132
133
|
{
|
|
133
134
|
title: 'Get apifm-admin SDK method info',
|
|
134
|
-
description:
|
|
135
|
+
description:
|
|
136
|
+
'Planning helper only. Returns metadata for one SDK method, including route, HTTP method, parameters, and return example. This is not live API data.',
|
|
135
137
|
inputSchema: z.object({
|
|
136
138
|
methodName: z.string().min(1)
|
|
137
139
|
})
|
|
@@ -150,7 +152,7 @@ server.registerTool(
|
|
|
150
152
|
{
|
|
151
153
|
title: 'Call an APIFM admin API',
|
|
152
154
|
description:
|
|
153
|
-
'Calls
|
|
155
|
+
'Primary execution tool. Calls an apifm-admin SDK method by name and returns the live backend API response in apiResult. Use this when the user wants actual data or an operation performed.',
|
|
154
156
|
inputSchema: z.object({
|
|
155
157
|
methodName: z.string().min(1).describe('apifm-admin SDK method name, for example userList.'),
|
|
156
158
|
payload: z.record(z.string(), z.any()).optional().describe('Non-sensitive SDK payload.'),
|
|
@@ -162,54 +164,59 @@ server.registerTool(
|
|
|
162
164
|
headers: z
|
|
163
165
|
.record(z.string(), z.string())
|
|
164
166
|
.optional()
|
|
165
|
-
.describe('Optional non-sensitive extra headers. Authorization and token headers are rejected.')
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
const sensitiveHeaderPath = containsSensitiveKey(headers)
|
|
176
|
-
if (sensitiveHeaderPath) {
|
|
177
|
-
return toolError(
|
|
178
|
-
`Refusing to call ${methodName}: headers contain a sensitive field at "${sensitiveHeaderPath}". Use apifm_admin_start_auth for secrets.`
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const account = getAccount(alias)
|
|
183
|
-
if (!account) {
|
|
184
|
-
return toolError(
|
|
185
|
-
'No APIFM admin account is authorized. Call apifm_admin_start_auth first, then use the returned local browser URL.'
|
|
186
|
-
)
|
|
167
|
+
.describe('Optional non-sensitive extra headers. Authorization and token headers are rejected.'),
|
|
168
|
+
includeMetadata: z
|
|
169
|
+
.boolean()
|
|
170
|
+
.optional()
|
|
171
|
+
.describe('Set true only when debugging. Defaults to false so the response stays focused on live API data.')
|
|
172
|
+
}),
|
|
173
|
+
annotations: {
|
|
174
|
+
openWorldHint: true
|
|
187
175
|
}
|
|
176
|
+
},
|
|
177
|
+
async ({ methodName, payload = {}, alias, domains, headers = {}, includeMetadata = false }) =>
|
|
178
|
+
callToolResult({ methodName, payload, alias, domains, headers, includeMetadata })
|
|
179
|
+
)
|
|
188
180
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
181
|
+
server.registerTool(
|
|
182
|
+
'apifm_admin_find_and_call',
|
|
183
|
+
{
|
|
184
|
+
title: 'Find and call an APIFM admin API',
|
|
185
|
+
description:
|
|
186
|
+
'Best primary tool for natural-language backend requests. Searches the installed apifm-admin SDK, selects the best matching callable method, calls it, and returns the live backend API response in apiResult.',
|
|
187
|
+
inputSchema: z.object({
|
|
188
|
+
query: z
|
|
189
|
+
.string()
|
|
190
|
+
.min(1)
|
|
191
|
+
.describe('Natural-language intent or keywords, for example 用户列表, 订单详情, 注册邮箱验证码.'),
|
|
192
|
+
payload: z.record(z.string(), z.any()).optional().describe('Non-sensitive SDK payload for the selected API.'),
|
|
193
|
+
alias: z.string().optional().describe('Optional local account alias. Defaults to the active account.'),
|
|
194
|
+
domains: z.record(z.string(), z.string().url()).optional().describe('Optional per-call domain overrides.'),
|
|
195
|
+
headers: z.record(z.string(), z.string()).optional().describe('Optional non-sensitive extra headers.'),
|
|
196
|
+
methodName: z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe('Optional exact method name. If provided, search is skipped and this method is called.'),
|
|
200
|
+
includeMetadata: z.boolean().optional().describe('Set true only when debugging.')
|
|
201
|
+
}),
|
|
202
|
+
annotations: {
|
|
203
|
+
openWorldHint: true
|
|
192
204
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
sdkHeaders.Authorization = account.basicAuth
|
|
205
|
+
},
|
|
206
|
+
async ({ query, payload = {}, alias, domains, headers = {}, methodName, includeMetadata = false }) => {
|
|
207
|
+
const selectedMethod = methodName || findCallableMethod(query)
|
|
208
|
+
if (!selectedMethod) {
|
|
209
|
+
return toolError(`No callable apifm-admin SDK method matched query: ${query}`)
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
methodName,
|
|
210
|
-
accountAlias: account.alias,
|
|
211
|
-
metadata: getMethodMetadata(methodName) ? summarizeMetadata(getMethodMetadata(methodName)) : null,
|
|
212
|
-
result: redactSensitive(result)
|
|
212
|
+
return callToolResult({
|
|
213
|
+
methodName: selectedMethod,
|
|
214
|
+
payload,
|
|
215
|
+
alias,
|
|
216
|
+
domains,
|
|
217
|
+
headers,
|
|
218
|
+
includeMetadata,
|
|
219
|
+
searchQuery: query
|
|
213
220
|
})
|
|
214
221
|
}
|
|
215
222
|
)
|
|
@@ -245,6 +252,76 @@ function toolJson(data) {
|
|
|
245
252
|
}
|
|
246
253
|
}
|
|
247
254
|
|
|
255
|
+
async function callToolResult({
|
|
256
|
+
methodName,
|
|
257
|
+
payload = {},
|
|
258
|
+
alias,
|
|
259
|
+
domains,
|
|
260
|
+
headers = {},
|
|
261
|
+
includeMetadata = false,
|
|
262
|
+
searchQuery
|
|
263
|
+
}) {
|
|
264
|
+
const sensitivePayloadPath = containsSensitiveKey(payload)
|
|
265
|
+
if (sensitivePayloadPath) {
|
|
266
|
+
return toolError(
|
|
267
|
+
`Refusing to call ${methodName}: payload contains a sensitive field at "${sensitivePayloadPath}". Use apifm_admin_start_auth for secrets.`
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
const sensitiveHeaderPath = containsSensitiveKey(headers)
|
|
271
|
+
if (sensitiveHeaderPath) {
|
|
272
|
+
return toolError(
|
|
273
|
+
`Refusing to call ${methodName}: headers contain a sensitive field at "${sensitiveHeaderPath}". Use apifm_admin_start_auth for secrets.`
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const account = getAccount(alias)
|
|
278
|
+
if (!account) {
|
|
279
|
+
return toolError(
|
|
280
|
+
'No APIFM admin account is authorized. Call apifm_admin_start_auth first, then use the returned local browser URL.'
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const sdk = getSdk()
|
|
285
|
+
if (typeof sdk[methodName] !== 'function') {
|
|
286
|
+
return toolError(`apifm-admin does not expose a function named ${methodName}. Search methods first.`)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
resetSdkConfig()
|
|
290
|
+
const allDomains = { ...(account.domains || {}), ...(domains || {}) }
|
|
291
|
+
const sdkHeaders = { ...headers }
|
|
292
|
+
if (account.basicAuth) {
|
|
293
|
+
sdkHeaders.Authorization = account.basicAuth
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
sdk.setConfig({
|
|
297
|
+
token: account.token || '',
|
|
298
|
+
headers: sdkHeaders,
|
|
299
|
+
domains: allDomains
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
const apiResult = redactSensitive(await sdk[methodName](payload))
|
|
303
|
+
const response = {
|
|
304
|
+
apiResult,
|
|
305
|
+
called: {
|
|
306
|
+
methodName,
|
|
307
|
+
accountAlias: account.alias,
|
|
308
|
+
searchQuery: searchQuery || undefined
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (includeMetadata) {
|
|
313
|
+
response.metadata = getMethodMetadata(methodName) ? summarizeMetadata(getMethodMetadata(methodName)) : null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return toolJson(response)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function findCallableMethod(query) {
|
|
320
|
+
const sdk = getSdk()
|
|
321
|
+
const candidates = searchMethods(query, 20)
|
|
322
|
+
return candidates.find((candidate) => typeof sdk[candidate.methodName] === 'function')?.methodName || ''
|
|
323
|
+
}
|
|
324
|
+
|
|
248
325
|
function toolError(message) {
|
|
249
326
|
return {
|
|
250
327
|
isError: true,
|
package/src/self-check.js
CHANGED
|
@@ -23,6 +23,7 @@ try {
|
|
|
23
23
|
'apifm_admin_accounts',
|
|
24
24
|
'apifm_admin_search_methods',
|
|
25
25
|
'apifm_admin_method_info',
|
|
26
|
+
'apifm_admin_find_and_call',
|
|
26
27
|
'apifm_admin_call'
|
|
27
28
|
]
|
|
28
29
|
for (const name of requiredTools) {
|
|
@@ -51,6 +52,17 @@ try {
|
|
|
51
52
|
throw new Error('Sensitive payload guard did not reject password fields.')
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
const findAndCall = await client.callTool({
|
|
56
|
+
name: 'apifm_admin_find_and_call',
|
|
57
|
+
arguments: {
|
|
58
|
+
query: '用户列表',
|
|
59
|
+
payload: { page: 1, pageSize: 1 }
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
if (!findAndCall.isError || !findAndCall.content[0].text.includes('No APIFM admin account is authorized')) {
|
|
63
|
+
throw new Error('find_and_call should attempt execution and require authorization before returning API data.')
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
await client.close()
|
|
55
67
|
console.log(`self-check passed: ${names.length} tools, ${searchData.sdkMethodCount} SDK methods discovered`)
|
|
56
68
|
} catch (error) {
|