@oneentry/mcp-platform-server 0.1.4 → 0.1.6
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 +29 -2
- package/dist/api/build-catalog.js +122 -17
- package/dist/api/catalog.d.ts +1 -0
- package/dist/api/catalog.js +22 -1
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +63 -5
- package/dist/api/normalize-schema.js +6 -1
- package/dist/api/operation-notes.d.ts +11 -0
- package/dist/api/operation-notes.js +394 -0
- package/dist/api/types.d.ts +17 -0
- package/dist/api/upload.d.ts +23 -0
- package/dist/api/upload.js +189 -0
- package/dist/bin/cli.js +7 -1
- package/dist/config/config.d.ts +37 -0
- package/dist/config/config.js +15 -0
- package/dist/knowledge/ru-en-terms.d.ts +1 -0
- package/dist/knowledge/ru-en-terms.js +67 -0
- package/dist/knowledge/search.js +2 -1
- package/dist/server.js +3 -0
- package/dist/tools/api-call.js +14 -1
- package/dist/tools/api-discovery.js +119 -13
- package/dist/tools/docs.js +4 -1
- package/dist/tools/guide.js +19 -3
- package/dist/tools/upload.d.ts +3 -0
- package/dist/tools/upload.js +229 -0
- package/dist/tools/whoami.js +5 -0
- package/knowledge/operating-rules.md +51 -44
- package/package.json +2 -1
package/dist/bin/cli.js
CHANGED
|
@@ -19,6 +19,11 @@ Options
|
|
|
19
19
|
--request-timeout <ms> per-request timeout, default 30000
|
|
20
20
|
--max-response-bytes <n> response cap handed to the model, default 24576
|
|
21
21
|
|
|
22
|
+
File uploads (cms_upload_file, cms_import_file_from_url)
|
|
23
|
+
--upload-root <dir> directory cms_upload_file may read from, default the working directory
|
|
24
|
+
--upload-max-bytes <n> size limit of one upload, default 26214400
|
|
25
|
+
--upload-allowed-hosts a,b hosts cms_import_file_from_url may fetch from; required in remote mode
|
|
26
|
+
|
|
22
27
|
Knowledge (documentation is fetched from GitHub, not bundled)
|
|
23
28
|
--knowledge-repo <o/n> knowledge repository, default ONEENTRY-PLATFORM/oneentry-platform-rules
|
|
24
29
|
--knowledge-ref <ref> branch, tag or commit, default main
|
|
@@ -31,7 +36,8 @@ Environment
|
|
|
31
36
|
ONEENTRY_CMS_LOGIN, ONEENTRY_CMS_PASSWORD, ONEENTRY_CMS_TOKEN, ONEENTRY_CMS_BASE_URL, ONEENTRY_MCP_ALLOW,
|
|
32
37
|
ONEENTRY_MCP_AUDIT_PATH, ONEENTRY_MCP_PORT, ONEENTRY_MCP_HOST, ONEENTRY_MCP_CACHE_DIR,
|
|
33
38
|
ONEENTRY_MCP_KNOWLEDGE_REPO, ONEENTRY_MCP_KNOWLEDGE_REF, ONEENTRY_MCP_KNOWLEDGE_PATH,
|
|
34
|
-
ONEENTRY_MCP_KNOWLEDGE_TTL_MS, ONEENTRY_MCP_OFFLINE, ONEENTRY_GITHUB_TOKEN
|
|
39
|
+
ONEENTRY_MCP_KNOWLEDGE_TTL_MS, ONEENTRY_MCP_OFFLINE, ONEENTRY_GITHUB_TOKEN,
|
|
40
|
+
ONEENTRY_MCP_UPLOAD_ROOT, ONEENTRY_MCP_UPLOAD_MAX_BYTES, ONEENTRY_MCP_UPLOAD_ALLOWED_HOSTS
|
|
35
41
|
`;
|
|
36
42
|
const main = async () => {
|
|
37
43
|
const argv = process.argv.slice(2);
|
package/dist/config/config.d.ts
CHANGED
|
@@ -26,6 +26,20 @@ export declare const KnowledgeConfigSchema: z.ZodObject<{
|
|
|
26
26
|
token?: string | undefined;
|
|
27
27
|
}>;
|
|
28
28
|
export type KnowledgeSettings = z.infer<typeof KnowledgeConfigSchema>;
|
|
29
|
+
export declare const UploadConfigSchema: z.ZodObject<{
|
|
30
|
+
root: z.ZodString;
|
|
31
|
+
maxBytes: z.ZodNumber;
|
|
32
|
+
allowedHosts: z.ZodArray<z.ZodString, "many">;
|
|
33
|
+
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
root: string;
|
|
35
|
+
maxBytes: number;
|
|
36
|
+
allowedHosts: string[];
|
|
37
|
+
}, {
|
|
38
|
+
root: string;
|
|
39
|
+
maxBytes: number;
|
|
40
|
+
allowedHosts: string[];
|
|
41
|
+
}>;
|
|
42
|
+
export type UploadSettings = z.infer<typeof UploadConfigSchema>;
|
|
29
43
|
export declare const ConfigSchema: z.ZodObject<{
|
|
30
44
|
mode: z.ZodEnum<["local", "remote"]>;
|
|
31
45
|
baseUrl: z.ZodString;
|
|
@@ -59,6 +73,19 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
59
73
|
auditPath: z.ZodOptional<z.ZodString>;
|
|
60
74
|
requestTimeoutMs: z.ZodNumber;
|
|
61
75
|
maxResponseBytes: z.ZodNumber;
|
|
76
|
+
upload: z.ZodObject<{
|
|
77
|
+
root: z.ZodString;
|
|
78
|
+
maxBytes: z.ZodNumber;
|
|
79
|
+
allowedHosts: z.ZodArray<z.ZodString, "many">;
|
|
80
|
+
}, "strip", z.ZodTypeAny, {
|
|
81
|
+
root: string;
|
|
82
|
+
maxBytes: number;
|
|
83
|
+
allowedHosts: string[];
|
|
84
|
+
}, {
|
|
85
|
+
root: string;
|
|
86
|
+
maxBytes: number;
|
|
87
|
+
allowedHosts: string[];
|
|
88
|
+
}>;
|
|
62
89
|
http: z.ZodObject<{
|
|
63
90
|
port: z.ZodNumber;
|
|
64
91
|
host: z.ZodString;
|
|
@@ -87,6 +114,11 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
87
114
|
cacheDir: string;
|
|
88
115
|
requestTimeoutMs: number;
|
|
89
116
|
maxResponseBytes: number;
|
|
117
|
+
upload: {
|
|
118
|
+
root: string;
|
|
119
|
+
maxBytes: number;
|
|
120
|
+
allowedHosts: string[];
|
|
121
|
+
};
|
|
90
122
|
http: {
|
|
91
123
|
port: number;
|
|
92
124
|
host: string;
|
|
@@ -111,6 +143,11 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
111
143
|
cacheDir: string;
|
|
112
144
|
requestTimeoutMs: number;
|
|
113
145
|
maxResponseBytes: number;
|
|
146
|
+
upload: {
|
|
147
|
+
root: string;
|
|
148
|
+
maxBytes: number;
|
|
149
|
+
allowedHosts: string[];
|
|
150
|
+
};
|
|
114
151
|
http: {
|
|
115
152
|
port: number;
|
|
116
153
|
host: string;
|
package/dist/config/config.js
CHANGED
|
@@ -12,6 +12,11 @@ export const KnowledgeConfigSchema = z.object({
|
|
|
12
12
|
offline: z.boolean(),
|
|
13
13
|
token: z.string().min(1).optional(),
|
|
14
14
|
});
|
|
15
|
+
export const UploadConfigSchema = z.object({
|
|
16
|
+
root: z.string().min(1),
|
|
17
|
+
maxBytes: z.number().int().positive(),
|
|
18
|
+
allowedHosts: z.array(z.string()),
|
|
19
|
+
});
|
|
15
20
|
export const ConfigSchema = z.object({
|
|
16
21
|
mode: ServerModeSchema,
|
|
17
22
|
baseUrl: z.string().url(),
|
|
@@ -24,6 +29,7 @@ export const ConfigSchema = z.object({
|
|
|
24
29
|
auditPath: z.string().optional(),
|
|
25
30
|
requestTimeoutMs: z.number().int().positive(),
|
|
26
31
|
maxResponseBytes: z.number().int().positive(),
|
|
32
|
+
upload: UploadConfigSchema,
|
|
27
33
|
http: z.object({
|
|
28
34
|
port: z.number().int().min(1).max(65535),
|
|
29
35
|
host: z.string().min(1),
|
|
@@ -40,6 +46,7 @@ const DEFAULTS = {
|
|
|
40
46
|
knowledgeRepo: 'ONEENTRY-PLATFORM/oneentry-platform-rules',
|
|
41
47
|
knowledgeRef: 'main',
|
|
42
48
|
knowledgeTtlMs: 3_600_000,
|
|
49
|
+
uploadMaxBytes: 26_214_400,
|
|
43
50
|
};
|
|
44
51
|
export const parseFlags = (argv) => {
|
|
45
52
|
const flags = {};
|
|
@@ -132,6 +139,7 @@ export const loadConfig = (argv, env = process.env, cwd = process.cwd()) => {
|
|
|
132
139
|
const pickKnowledge = (flagKey, envKey, fileKey) => asString(flags[flagKey]) ?? asString(env[envKey]) ?? knowledgeFile(fileKey);
|
|
133
140
|
const mode = flags['http'] === true || asString(flags['http']) ? 'remote' : 'local';
|
|
134
141
|
const originsRaw = pick('allowed-origins', 'ONEENTRY_MCP_ALLOWED_ORIGINS', 'allowedOrigins');
|
|
142
|
+
const uploadHostsRaw = pick('upload-allowed-hosts', 'ONEENTRY_MCP_UPLOAD_ALLOWED_HOSTS', 'uploadAllowedHosts');
|
|
135
143
|
const candidate = {
|
|
136
144
|
mode,
|
|
137
145
|
baseUrl: normalizeBaseUrl(pick('base-url', 'ONEENTRY_CMS_BASE_URL', 'baseUrl') ?? DEFAULTS.baseUrl),
|
|
@@ -153,6 +161,13 @@ export const loadConfig = (argv, env = process.env, cwd = process.cwd()) => {
|
|
|
153
161
|
auditPath: pick('audit', 'ONEENTRY_MCP_AUDIT_PATH', 'auditPath'),
|
|
154
162
|
requestTimeoutMs: asNumber(pick('request-timeout', 'ONEENTRY_MCP_REQUEST_TIMEOUT_MS', 'requestTimeoutMs'), DEFAULTS.requestTimeoutMs),
|
|
155
163
|
maxResponseBytes: asNumber(pick('max-response-bytes', 'ONEENTRY_MCP_MAX_RESPONSE_BYTES', 'maxResponseBytes'), DEFAULTS.maxResponseBytes),
|
|
164
|
+
upload: {
|
|
165
|
+
root: pick('upload-root', 'ONEENTRY_MCP_UPLOAD_ROOT', 'uploadRoot') ?? cwd,
|
|
166
|
+
maxBytes: asNumber(pick('upload-max-bytes', 'ONEENTRY_MCP_UPLOAD_MAX_BYTES', 'uploadMaxBytes'), DEFAULTS.uploadMaxBytes),
|
|
167
|
+
allowedHosts: uploadHostsRaw
|
|
168
|
+
? uploadHostsRaw.split(',').map((host) => host.trim().toLowerCase()).filter(Boolean)
|
|
169
|
+
: [],
|
|
170
|
+
},
|
|
156
171
|
http: {
|
|
157
172
|
port: asNumber(pick('port', 'ONEENTRY_MCP_PORT', 'port'), DEFAULTS.httpPort),
|
|
158
173
|
host: pick('host', 'ONEENTRY_MCP_HOST', 'host') ?? DEFAULTS.httpHost,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const withEnglishTerms: (query: string) => string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const RU_EN = {
|
|
2
|
+
атрибут: 'attribute',
|
|
3
|
+
блок: 'block',
|
|
4
|
+
валидатор: 'validator',
|
|
5
|
+
вложен: 'nested',
|
|
6
|
+
выгрузк: 'export',
|
|
7
|
+
групп: 'group',
|
|
8
|
+
доступ: 'permission access',
|
|
9
|
+
заказ: 'order',
|
|
10
|
+
запрос: 'request',
|
|
11
|
+
значени: 'value',
|
|
12
|
+
изображени: 'image file',
|
|
13
|
+
импорт: 'import',
|
|
14
|
+
индекс: 'index',
|
|
15
|
+
каталог: 'catalogue catalog',
|
|
16
|
+
картинк: 'image file',
|
|
17
|
+
категори: 'page category',
|
|
18
|
+
локал: 'locale',
|
|
19
|
+
маркер: 'marker identifier',
|
|
20
|
+
меню: 'menu',
|
|
21
|
+
модул: 'module',
|
|
22
|
+
набор: 'set',
|
|
23
|
+
настройк: 'settings',
|
|
24
|
+
ошибк: 'error',
|
|
25
|
+
пагинац: 'pagination limit offset',
|
|
26
|
+
парамет: 'parameter',
|
|
27
|
+
перевод: 'localization locale',
|
|
28
|
+
подтвержд: 'confirm',
|
|
29
|
+
поиск: 'search',
|
|
30
|
+
позиц: 'position',
|
|
31
|
+
польз: 'user',
|
|
32
|
+
превью: 'preview',
|
|
33
|
+
прав: 'permission',
|
|
34
|
+
продукт: 'product',
|
|
35
|
+
сортиров: 'sort order',
|
|
36
|
+
ссылк: 'link',
|
|
37
|
+
статус: 'status',
|
|
38
|
+
страниц: 'page',
|
|
39
|
+
схем: 'schema',
|
|
40
|
+
товар: 'product',
|
|
41
|
+
тип: 'type',
|
|
42
|
+
файл: 'file upload',
|
|
43
|
+
фильтр: 'filter',
|
|
44
|
+
форм: 'form',
|
|
45
|
+
шаблон: 'template',
|
|
46
|
+
язык: 'language locale',
|
|
47
|
+
};
|
|
48
|
+
const STEMS = Object.keys(RU_EN).sort((a, b) => b.length - a.length);
|
|
49
|
+
export const withEnglishTerms = (query) => {
|
|
50
|
+
const lower = query.toLowerCase();
|
|
51
|
+
if (!/[а-яё]/.test(lower)) {
|
|
52
|
+
return query;
|
|
53
|
+
}
|
|
54
|
+
const added = new Set();
|
|
55
|
+
for (const word of lower.split(/[^\p{L}\p{N}]+/u)) {
|
|
56
|
+
if (word === '') {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const stem = STEMS.find((candidate) => word.startsWith(candidate));
|
|
60
|
+
if (stem) {
|
|
61
|
+
for (const term of RU_EN[stem]?.split(' ') ?? []) {
|
|
62
|
+
added.add(term);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return added.size > 0 ? `${query} ${[...added].join(' ')}` : query;
|
|
67
|
+
};
|
package/dist/knowledge/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import MiniSearch from 'minisearch';
|
|
2
|
+
import { withEnglishTerms } from './ru-en-terms.js';
|
|
2
3
|
const REPO_WEIGHT = { mcp: 1.6, back: 1, front: 0.9 };
|
|
3
4
|
const KIND_WEIGHT = {
|
|
4
5
|
docs: 1.15,
|
|
@@ -69,7 +70,7 @@ export class KnowledgeIndex {
|
|
|
69
70
|
}
|
|
70
71
|
search(query, limit = 8) {
|
|
71
72
|
const terms = query.split(/\s+/).filter((t) => t.length > 1);
|
|
72
|
-
const raw = this.mini.search(meaningfulQuery(query));
|
|
73
|
+
const raw = this.mini.search(withEnglishTerms(meaningfulQuery(query)));
|
|
73
74
|
const hits = [];
|
|
74
75
|
for (const result of raw) {
|
|
75
76
|
const chunk = this.byId.get(String(result.id));
|
package/dist/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerApiCall } from './tools/api-call.js';
|
|
|
9
9
|
import { registerApiDiscovery } from './tools/api-discovery.js';
|
|
10
10
|
import { registerDocs } from './tools/docs.js';
|
|
11
11
|
import { registerGuide } from './tools/guide.js';
|
|
12
|
+
import { registerUpload } from './tools/upload.js';
|
|
12
13
|
import { registerWhoami } from './tools/whoami.js';
|
|
13
14
|
const SERVER_VERSION = (() => {
|
|
14
15
|
try {
|
|
@@ -58,6 +59,7 @@ export const createServer = (deps, getSession) => {
|
|
|
58
59
|
const server = new McpServer({ name: 'oneentry-mcp-platform', version: SERVER_VERSION }, {
|
|
59
60
|
instructions: 'OneEntry CMS Admin API. Call cms_guide first, then cms_docs_read on "mcp/operating-rules" before any write. ' +
|
|
60
61
|
'Discover endpoints with cms_api_search, get payload shapes with cms_api_describe, execute with cms_api_call. ' +
|
|
62
|
+
'Files are the exception: cms_api_call cannot send multipart, so upload with cms_upload_file or cms_import_file_from_url. ' +
|
|
61
63
|
'Never construct paths or operation ids by hand.',
|
|
62
64
|
});
|
|
63
65
|
registerGuide(server, deps);
|
|
@@ -65,5 +67,6 @@ export const createServer = (deps, getSession) => {
|
|
|
65
67
|
registerDocs(server, deps);
|
|
66
68
|
registerApiDiscovery(server, deps);
|
|
67
69
|
registerApiCall(server, getSession);
|
|
70
|
+
registerUpload(server, getSession);
|
|
68
71
|
return server;
|
|
69
72
|
};
|
package/dist/tools/api-call.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { AuditLog } from '../api/audit.js';
|
|
3
|
-
import { buildUrl, normalizeBody, RequestBuildError } from '../api/client.js';
|
|
3
|
+
import { buildUrl, normalizeBody, RequestBuildError, unsupportedBodyFormat, } from '../api/client.js';
|
|
4
4
|
import { checkLevel, decide } from '../api/policy.js';
|
|
5
5
|
import { shapeResponse, summarizeTarget } from '../api/shape.js';
|
|
6
6
|
import { errorResult, jsonResult } from './result.js';
|
|
@@ -40,6 +40,10 @@ export const registerApiCall = (server, getSession) => {
|
|
|
40
40
|
hint: 'Operation ids come from cms_api_search — do not construct them by hand.',
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
+
const formatDenial = unsupportedBodyFormat(operation);
|
|
44
|
+
if (formatDenial) {
|
|
45
|
+
return errorResult(formatDenial, { opId, contentType: operation.body?.contentType });
|
|
46
|
+
}
|
|
43
47
|
const normalizedBody = normalizeBody(body);
|
|
44
48
|
const args = {
|
|
45
49
|
...(path ? { path } : {}),
|
|
@@ -141,6 +145,15 @@ export const registerApiCall = (server, getSession) => {
|
|
|
141
145
|
status: result.status,
|
|
142
146
|
truncated: shaped.truncated,
|
|
143
147
|
body: shaped.body,
|
|
148
|
+
...(operation.verifyWith
|
|
149
|
+
? {
|
|
150
|
+
verifyWith: operation.verifyWith,
|
|
151
|
+
next: `This write is not confirmed by its status. Read it back with ` +
|
|
152
|
+
`${operation.verifyWith.opId} and check "${operation.verifyWith.check}" ` +
|
|
153
|
+
'before reporting success.',
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
...(operation.silentNoOp ? { silentNoOp: operation.silentNoOp } : {}),
|
|
144
157
|
});
|
|
145
158
|
});
|
|
146
159
|
};
|
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { unsupportedBodyFormat } from '../api/client.js';
|
|
2
3
|
import { errorResult, jsonResult } from './result.js';
|
|
4
|
+
const sampleValue = (schema, example) => {
|
|
5
|
+
if (example !== undefined) {
|
|
6
|
+
return example;
|
|
7
|
+
}
|
|
8
|
+
if (schema.example !== undefined) {
|
|
9
|
+
return schema.example;
|
|
10
|
+
}
|
|
11
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
12
|
+
return schema.enum[0];
|
|
13
|
+
}
|
|
14
|
+
switch (schema.type) {
|
|
15
|
+
case 'number':
|
|
16
|
+
case 'integer':
|
|
17
|
+
return 0;
|
|
18
|
+
case 'boolean':
|
|
19
|
+
return false;
|
|
20
|
+
case 'array':
|
|
21
|
+
return [];
|
|
22
|
+
case 'object':
|
|
23
|
+
return {};
|
|
24
|
+
default:
|
|
25
|
+
return `<${schema['x-source-type'] ?? schema.type ?? 'value'}>`;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const isWorthShowing = (param) => param.required ||
|
|
29
|
+
param.example !== undefined ||
|
|
30
|
+
param.schema.example !== undefined ||
|
|
31
|
+
param.schema.default !== undefined;
|
|
32
|
+
const argsFor = (params, location, filter) => {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const param of params) {
|
|
35
|
+
if (param.location !== location || !filter(param)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
out[param.name] = sampleValue(param.schema, param.example);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
};
|
|
42
|
+
const callExample = (operation) => {
|
|
43
|
+
const path = argsFor(operation.params, 'path', () => true);
|
|
44
|
+
const query = argsFor(operation.params, 'query', isWorthShowing);
|
|
45
|
+
return {
|
|
46
|
+
opId: operation.opId,
|
|
47
|
+
...(Object.keys(path).length > 0 ? { path } : {}),
|
|
48
|
+
...(Object.keys(query).length > 0 ? { query } : {}),
|
|
49
|
+
...(operation.body?.schema.example !== undefined
|
|
50
|
+
? { body: operation.body.schema.example }
|
|
51
|
+
: {}),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
3
54
|
export const registerApiDiscovery = (server, deps) => {
|
|
4
55
|
const { catalog } = deps;
|
|
5
56
|
server.registerTool('cms_api_search', {
|
|
@@ -22,10 +73,16 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
22
73
|
...(limit ? { limit } : {}),
|
|
23
74
|
});
|
|
24
75
|
if (hits.length === 0) {
|
|
76
|
+
const unexposed = catalog.unexposedMatches(query);
|
|
25
77
|
return jsonResult({
|
|
26
78
|
hits: [],
|
|
27
79
|
tags: [...new Set(catalog.operations().map((o) => o.tag))].sort(),
|
|
28
|
-
|
|
80
|
+
...(unexposed.length > 0 ? { knownButNotExposed: unexposed } : {}),
|
|
81
|
+
hint: unexposed.length > 0
|
|
82
|
+
? 'Nothing matched in this catalog, but the platform does know the names under ' +
|
|
83
|
+
'"knownButNotExposed" — this instance simply does not serve them, so no path ' +
|
|
84
|
+
'you construct will reach them. Do not guess URLs: report them as unavailable here.'
|
|
85
|
+
: 'Nothing matched. Try a bare entity name, or filter by one of the tags listed here.',
|
|
29
86
|
});
|
|
30
87
|
}
|
|
31
88
|
return jsonResult({
|
|
@@ -35,7 +92,7 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
35
92
|
});
|
|
36
93
|
server.registerTool('cms_api_describe', {
|
|
37
94
|
title: 'Describe an Admin API operation',
|
|
38
|
-
description: 'Full detail for one operation: path and query parameters, request-body schema, required permission, risk level, and whether it is permanently confirm-gated. Fields marked "x-loose": true could not be converted to a JSON Schema type — trust their example, not their type.',
|
|
95
|
+
description: 'Full detail for one operation: a ready-to-copy "example" call, path and query parameters, request-body schema, required permission, risk level, and whether it is permanently confirm-gated. Fields marked "x-loose": true could not be converted to a JSON Schema type — trust their example, not their type. Where a body shape has been verified on a live instance it is returned as "curatedBody", which wins over the document\'s own example wherever the two disagree. Where the operation is known to answer success without doing the work, "silentNoOp" and "verifyWith" say so and name the read that proves it.',
|
|
39
96
|
inputSchema: {
|
|
40
97
|
opId: z.string().min(1).describe('Operation id from cms_api_search, e.g. "AdminPagesController_findAllRoot".'),
|
|
41
98
|
},
|
|
@@ -43,18 +100,28 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
43
100
|
}, ({ opId }) => {
|
|
44
101
|
const operation = catalog.get(opId);
|
|
45
102
|
if (!operation) {
|
|
103
|
+
const unexposed = catalog.unexposedMatches(opId);
|
|
46
104
|
return errorResult(`Unknown opId "${opId}".`, {
|
|
47
105
|
didYouMean: catalog.suggest(opId),
|
|
48
|
-
|
|
106
|
+
...(unexposed.length > 0 ? { knownButNotExposed: unexposed } : {}),
|
|
107
|
+
hint: unexposed.length > 0
|
|
108
|
+
? 'The platform knows this name, but this instance does not serve the operation, ' +
|
|
109
|
+
'so it cannot be called from here by any path. Report it as unavailable.'
|
|
110
|
+
: 'Operation ids come from cms_api_search — do not construct them by hand.',
|
|
49
111
|
});
|
|
50
112
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: [])
|
|
57
|
-
|
|
113
|
+
const formatDenial = unsupportedBodyFormat(operation);
|
|
114
|
+
const properties = operation.body?.schema.properties;
|
|
115
|
+
const loose = properties
|
|
116
|
+
? Object.entries(properties)
|
|
117
|
+
.filter(([, schema]) => schema['x-loose'] === true)
|
|
118
|
+
.map(([name, schema]) => ({ field: name, sourceType: schema['x-source-type'] }))
|
|
119
|
+
: [];
|
|
120
|
+
const mismatched = properties
|
|
121
|
+
? Object.entries(properties)
|
|
122
|
+
.filter(([, schema]) => schema['x-example-mismatch'] !== undefined)
|
|
123
|
+
.map(([name, schema]) => ({ field: name, problem: schema['x-example-mismatch'] }))
|
|
124
|
+
: [];
|
|
58
125
|
return jsonResult({
|
|
59
126
|
opId: operation.opId,
|
|
60
127
|
method: operation.method.toUpperCase(),
|
|
@@ -66,12 +133,51 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
66
133
|
risk: operation.risk,
|
|
67
134
|
alwaysConfirm: operation.alwaysConfirm,
|
|
68
135
|
params: operation.params,
|
|
136
|
+
paramsByLocation: {
|
|
137
|
+
path: operation.params.filter((p) => p.location === 'path').map((p) => p.name),
|
|
138
|
+
query: operation.params.filter((p) => p.location === 'query').map((p) => p.name),
|
|
139
|
+
},
|
|
69
140
|
body: operation.body ?? null,
|
|
141
|
+
example: callExample(operation),
|
|
70
142
|
looseFields: loose,
|
|
143
|
+
...(mismatched.length > 0
|
|
144
|
+
? {
|
|
145
|
+
exampleMismatches: mismatched,
|
|
146
|
+
exampleMismatchHint: 'These fields declare one type and give an example of another. The example is ' +
|
|
147
|
+
'the contract — the instance accepts what the example shows.',
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
...(operation.curatedExample !== undefined
|
|
151
|
+
? {
|
|
152
|
+
curatedBody: operation.curatedExample,
|
|
153
|
+
curatedBodySource: 'Verified on a live instance. Where this disagrees with "example" or with the ' +
|
|
154
|
+
'body schema above, this is the shape the instance and the admin panel read — ' +
|
|
155
|
+
'copy it and read "note" for what each part of it prevents.',
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
...(operation.note ? { note: operation.note } : {}),
|
|
159
|
+
...(operation.silentNoOp ? { silentNoOp: operation.silentNoOp } : {}),
|
|
160
|
+
...(operation.verifyWith ? { verifyWith: operation.verifyWith } : {}),
|
|
161
|
+
...(operation.body?.schema['x-unresolved'] === true
|
|
162
|
+
? {
|
|
163
|
+
bodySchemaUnresolved: 'The document does not resolve this body schema, so the empty "properties" ' +
|
|
164
|
+
'above means "unknown", not "no fields". Build the body from "example"; ' +
|
|
165
|
+
'if there is none, ask the human rather than guessing.',
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
...(formatDenial ? { notExecutable: formatDenial } : {}),
|
|
71
169
|
responseSummary: operation.responseSummary ?? null,
|
|
72
|
-
next:
|
|
73
|
-
?
|
|
74
|
-
|
|
170
|
+
next: formatDenial
|
|
171
|
+
? operation.body?.contentType === 'multipart/form-data'
|
|
172
|
+
? 'Do not call this with cms_api_call — upload with cms_upload_file or cms_import_file_from_url.'
|
|
173
|
+
: 'Do not call this operation — report it as unavailable through MCP.'
|
|
174
|
+
: operation.risk === 'read'
|
|
175
|
+
? 'Call it with cms_api_call, copying the "example" above.'
|
|
176
|
+
: operation.verifyWith
|
|
177
|
+
? 'Search the knowledge base with cms_docs_search for this entity, call cms_api_call ' +
|
|
178
|
+
`with dryRun: true first, and afterwards read the result back with ` +
|
|
179
|
+
`${operation.verifyWith.opId} — a success status is not evidence here.`
|
|
180
|
+
: 'Search the knowledge base with cms_docs_search for this entity, then call cms_api_call with dryRun: true first.',
|
|
75
181
|
});
|
|
76
182
|
});
|
|
77
183
|
};
|
package/dist/tools/docs.js
CHANGED
|
@@ -16,7 +16,10 @@ export const registerDocs = (server, deps) => {
|
|
|
16
16
|
if (hits.length === 0) {
|
|
17
17
|
return jsonResult({
|
|
18
18
|
hits: [],
|
|
19
|
-
hint: 'Nothing matched.
|
|
19
|
+
hint: 'Nothing matched. The knowledge base is written in English — ask in English, even ' +
|
|
20
|
+
'when the conversation is not. Try a module name (menus, orders, blocks, ' +
|
|
21
|
+
'attributes) or an entity field name. An empty result means this query found ' +
|
|
22
|
+
'nothing, not that the CMS has no documentation for it.',
|
|
20
23
|
});
|
|
21
24
|
}
|
|
22
25
|
return jsonResult({
|
package/dist/tools/guide.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ALWAYS_CONFIRM_PREFIXES } from '../api/operation-notes.js';
|
|
1
2
|
import { textResult } from './result.js';
|
|
2
3
|
const TAG_LIMIT = 24;
|
|
3
4
|
export const renderGuide = (deps) => {
|
|
@@ -39,7 +40,10 @@ export const renderGuide = (deps) => {
|
|
|
39
40
|
'2. `cms_docs_search` — find the reference doc for the entity you are touching;',
|
|
40
41
|
' `mcp/docs/server/doc-map` lists every document with a one-line "read this when".',
|
|
41
42
|
'3. `cms_api_search` → `cms_api_describe` — get the real operation and its payload shape.',
|
|
42
|
-
'
|
|
43
|
+
' Where `cms_api_describe` returns `curatedBody`, that shape was verified on a live instance',
|
|
44
|
+
' and wins over the document\'s own example.',
|
|
45
|
+
'4. `cms_api_call` — `dryRun: true` first for anything that mutates. Files go through',
|
|
46
|
+
' `cms_upload_file` or `cms_import_file_from_url` instead.',
|
|
43
47
|
'',
|
|
44
48
|
'Never invent a path: `cms_api_search` is the only authority on what exists.',
|
|
45
49
|
'',
|
|
@@ -50,8 +54,20 @@ export const renderGuide = (deps) => {
|
|
|
50
54
|
'## Hard limits',
|
|
51
55
|
'',
|
|
52
56
|
'- Admin API only. The Content and Developer APIs are deliberately not exposed.',
|
|
53
|
-
|
|
54
|
-
'
|
|
57
|
+
`- Mutations under ${ALWAYS_CONFIRM_PREFIXES.map((p) => `\`${p}\``).join(', ')} are`,
|
|
58
|
+
' permanently confirm-gated, at every allow level.',
|
|
59
|
+
'- **`cms_api_call` sends JSON only.** The file upload endpoint wants `multipart/form-data`,',
|
|
60
|
+
' so it has two tools of its own: `cms_upload_file` for a file on the machine running this',
|
|
61
|
+
' server, `cms_import_file_from_url` for one this server downloads. Both need',
|
|
62
|
+
' `--allow=write`, both are audited, both support `dryRun`, and both refuse a source outside',
|
|
63
|
+
' the operator\'s bounds. Pass `template` — the numeric id of a `/template-previews` record —',
|
|
64
|
+
' or the file is stored with no preview and nothing reports it.',
|
|
65
|
+
'- **The knowledge base is written in English.** Search it in English whatever language the',
|
|
66
|
+
' conversation is in; an empty result is a failed query, not a missing document.',
|
|
67
|
+
'- A success status is not evidence that the write landed. Where an operation is known to',
|
|
68
|
+
' answer success without doing the work, `cms_api_describe` says so under `silentNoOp` and',
|
|
69
|
+
' names the read that proves it under `verifyWith`. Verify with the read the *consumer*',
|
|
70
|
+
' uses, not the one you wrote to.',
|
|
55
71
|
'- Responses are capped; overflow is reported as `_truncated`. Narrow the query instead.',
|
|
56
72
|
...(catalog.catalog.warnings.length > 0
|
|
57
73
|
? ['', '## Warnings', '', ...catalog.catalog.warnings.map((w) => `- ${w}`)]
|