@oneentry/mcp-platform-server 0.1.0 → 0.1.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/LICENSE +21 -0
- package/README.md +135 -92
- package/data/permissions.json +411 -0
- package/dist/api/audit.d.ts +0 -11
- package/dist/api/audit.js +0 -10
- package/dist/api/auth.d.ts +0 -18
- package/dist/api/auth.js +0 -20
- package/dist/api/build-catalog.d.ts +10 -0
- package/dist/api/build-catalog.js +187 -0
- package/dist/api/catalog.d.ts +8 -15
- package/dist/api/catalog.js +54 -16
- package/dist/api/client.d.ts +0 -24
- package/dist/api/client.js +6 -28
- package/dist/api/normalize-schema.d.ts +3 -0
- package/dist/api/normalize-schema.js +133 -0
- package/dist/api/policy.d.ts +0 -23
- package/dist/api/policy.js +0 -0
- package/dist/api/shape.d.ts +0 -13
- package/dist/api/shape.js +0 -21
- package/dist/api/swagger-source.d.ts +11 -0
- package/dist/api/swagger-source.js +58 -0
- package/dist/api/types.d.ts +0 -34
- package/dist/api/types.js +0 -1
- package/dist/bin/cli.d.ts +0 -1
- package/dist/bin/cli.js +11 -7
- package/dist/config/config.d.ts +66 -46
- package/dist/config/config.js +47 -44
- package/dist/index.d.ts +9 -7
- package/dist/index.js +5 -6
- package/dist/knowledge/chunk.d.ts +0 -15
- package/dist/knowledge/chunk.js +0 -16
- package/dist/knowledge/github.d.ts +21 -0
- package/dist/knowledge/github.js +174 -0
- package/dist/knowledge/loader.d.ts +23 -24
- package/dist/knowledge/loader.js +69 -108
- package/dist/knowledge/search.d.ts +1 -12
- package/dist/knowledge/search.js +19 -20
- package/dist/knowledge/tar.d.ts +7 -0
- package/dist/knowledge/tar.js +87 -0
- package/dist/knowledge/types.d.ts +8 -22
- package/dist/knowledge/types.js +0 -1
- package/dist/server.d.ts +1 -12
- package/dist/server.js +21 -18
- package/dist/session.d.ts +1 -18
- package/dist/session.js +3 -13
- package/dist/tools/api-call.d.ts +0 -2
- package/dist/tools/api-call.js +0 -23
- package/dist/tools/api-discovery.d.ts +0 -2
- package/dist/tools/api-discovery.js +2 -5
- package/dist/tools/docs.d.ts +0 -2
- package/dist/tools/docs.js +1 -5
- package/dist/tools/guide.d.ts +0 -7
- package/dist/tools/guide.js +11 -11
- package/dist/tools/result.d.ts +0 -8
- package/dist/tools/result.js +0 -7
- package/dist/tools/whoami.d.ts +0 -2
- package/dist/tools/whoami.js +5 -3
- package/dist/transports/http.d.ts +0 -6
- package/dist/transports/http.js +17 -18
- package/dist/transports/stdio.d.ts +0 -6
- package/dist/transports/stdio.js +1 -7
- package/knowledge/operating-rules.md +121 -0
- package/package.json +20 -7
- package/data/catalog.json +0 -1
- package/data/knowledge.json +0 -1
- package/dist/api/audit.d.ts.map +0 -1
- package/dist/api/audit.js.map +0 -1
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js.map +0 -1
- package/dist/api/catalog.d.ts.map +0 -1
- package/dist/api/catalog.js.map +0 -1
- package/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js.map +0 -1
- package/dist/api/policy.d.ts.map +0 -1
- package/dist/api/policy.js.map +0 -1
- package/dist/api/shape.d.ts.map +0 -1
- package/dist/api/shape.js.map +0 -1
- package/dist/api/types.d.ts.map +0 -1
- package/dist/api/types.js.map +0 -1
- package/dist/bin/cli.d.ts.map +0 -1
- package/dist/bin/cli.js.map +0 -1
- package/dist/config/config.d.ts.map +0 -1
- package/dist/config/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/knowledge/chunk.d.ts.map +0 -1
- package/dist/knowledge/chunk.js.map +0 -1
- package/dist/knowledge/loader.d.ts.map +0 -1
- package/dist/knowledge/loader.js.map +0 -1
- package/dist/knowledge/search.d.ts.map +0 -1
- package/dist/knowledge/search.js.map +0 -1
- package/dist/knowledge/types.d.ts.map +0 -1
- package/dist/knowledge/types.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/tools/api-call.d.ts.map +0 -1
- package/dist/tools/api-call.js.map +0 -1
- package/dist/tools/api-discovery.d.ts.map +0 -1
- package/dist/tools/api-discovery.js.map +0 -1
- package/dist/tools/docs.d.ts.map +0 -1
- package/dist/tools/docs.js.map +0 -1
- package/dist/tools/guide.d.ts.map +0 -1
- package/dist/tools/guide.js.map +0 -1
- package/dist/tools/result.d.ts.map +0 -1
- package/dist/tools/result.js.map +0 -1
- package/dist/tools/whoami.d.ts.map +0 -1
- package/dist/tools/whoami.js.map +0 -1
- package/dist/transports/http.d.ts.map +0 -1
- package/dist/transports/http.js.map +0 -1
- package/dist/transports/stdio.d.ts.map +0 -1
- package/dist/transports/stdio.js.map +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
const BLOCK = 512;
|
|
3
|
+
const FIELD = {
|
|
4
|
+
name: [0, 100],
|
|
5
|
+
size: [124, 12],
|
|
6
|
+
typeflag: [156, 1],
|
|
7
|
+
prefix: [345, 155],
|
|
8
|
+
};
|
|
9
|
+
const readString = (block, [offset, length]) => {
|
|
10
|
+
const raw = block.subarray(offset, offset + length);
|
|
11
|
+
const end = raw.indexOf(0);
|
|
12
|
+
return raw.subarray(0, end === -1 ? raw.length : end).toString('utf8').trim();
|
|
13
|
+
};
|
|
14
|
+
const readSize = (block) => {
|
|
15
|
+
const [offset, length] = FIELD.size;
|
|
16
|
+
const first = block[offset] ?? 0;
|
|
17
|
+
if ((first & 0x80) !== 0) {
|
|
18
|
+
throw new Error('tar: base-256 size fields are not supported');
|
|
19
|
+
}
|
|
20
|
+
const text = readString(block, FIELD.size).replace(/\0/g, '').trim();
|
|
21
|
+
const parsed = Number.parseInt(text === '' ? '0' : text, 8);
|
|
22
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
23
|
+
throw new Error(`tar: unreadable size field "${text}" at offset ${String(offset + length)}`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
};
|
|
27
|
+
const readPaxPath = (data) => {
|
|
28
|
+
const text = data.toString('utf8');
|
|
29
|
+
const match = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(text);
|
|
30
|
+
return match?.[1];
|
|
31
|
+
};
|
|
32
|
+
const fullName = (block) => {
|
|
33
|
+
const name = readString(block, FIELD.name);
|
|
34
|
+
const prefix = readString(block, FIELD.prefix);
|
|
35
|
+
return prefix === '' ? name : `${prefix}/${name}`;
|
|
36
|
+
};
|
|
37
|
+
const isZeroBlock = (block) => {
|
|
38
|
+
for (const byte of block) {
|
|
39
|
+
if (byte !== 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
export const readTar = (buffer) => {
|
|
46
|
+
const entries = [];
|
|
47
|
+
let offset = 0;
|
|
48
|
+
let pendingName;
|
|
49
|
+
while (offset + BLOCK <= buffer.length) {
|
|
50
|
+
const header = buffer.subarray(offset, offset + BLOCK);
|
|
51
|
+
if (isZeroBlock(header)) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const size = readSize(header);
|
|
55
|
+
const typeflag = readString(header, FIELD.typeflag);
|
|
56
|
+
const dataStart = offset + BLOCK;
|
|
57
|
+
const dataEnd = dataStart + size;
|
|
58
|
+
if (dataEnd > buffer.length) {
|
|
59
|
+
throw new Error('tar: truncated archive');
|
|
60
|
+
}
|
|
61
|
+
const data = buffer.subarray(dataStart, dataEnd);
|
|
62
|
+
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
63
|
+
if (typeflag === 'x' || typeflag === 'X') {
|
|
64
|
+
pendingName = readPaxPath(data) ?? pendingName;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (typeflag === 'L') {
|
|
68
|
+
pendingName = data.toString('utf8').replace(/\0+$/, '');
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (typeflag === 'g') {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const name = pendingName ?? fullName(header);
|
|
75
|
+
pendingName = undefined;
|
|
76
|
+
if (typeflag !== '' && typeflag !== '0') {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
entries.push({ path: name, content: data.toString('utf8') });
|
|
80
|
+
}
|
|
81
|
+
return entries;
|
|
82
|
+
};
|
|
83
|
+
export const stripRoot = (entries) => entries.map((entry) => {
|
|
84
|
+
const cut = entry.path.indexOf('/');
|
|
85
|
+
return cut === -1 ? entry : { path: entry.path.slice(cut + 1), content: entry.content };
|
|
86
|
+
});
|
|
87
|
+
export const readTarGz = (buffer) => stripRoot(readTar(gunzipSync(buffer)));
|
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Один раздел документации — единица поиска и чтения.
|
|
3
|
-
* Резать документы на разделы обязательно: отдельные файлы доходят до 56 КБ,
|
|
4
|
-
* а модели нужен ровно тот раздел, который отвечает на её вопрос.
|
|
5
|
-
*/
|
|
6
1
|
export interface KnowledgeChunk {
|
|
7
|
-
/** Стабильный идентификатор `<docId>#<anchor>`. */
|
|
8
2
|
id: string;
|
|
9
|
-
/** Идентификатор документа, например `back/docs/menus-module`. */
|
|
10
3
|
docId: string;
|
|
11
|
-
|
|
12
|
-
repo: 'back' | 'front' | 'mcp';
|
|
13
|
-
/** Путь файла относительно корня репозитория-источника. */
|
|
4
|
+
repo: string;
|
|
14
5
|
path: string;
|
|
15
|
-
/** Слаг заголовка раздела; пустая строка — преамбула документа. */
|
|
16
6
|
anchor: string;
|
|
17
|
-
/** Текст заголовка раздела. */
|
|
18
7
|
title: string;
|
|
19
|
-
/** Заголовок документа (первый `#`), нужен для ранжирования и вывода. */
|
|
20
8
|
docTitle: string;
|
|
21
|
-
/** Тело раздела без заголовка. */
|
|
22
9
|
text: string;
|
|
23
10
|
}
|
|
24
|
-
/** Документ целиком: список его разделов, чтобы агент мог листать соседние. */
|
|
25
11
|
export interface KnowledgeDoc {
|
|
26
12
|
docId: string;
|
|
27
|
-
repo:
|
|
13
|
+
repo: string;
|
|
28
14
|
path: string;
|
|
29
15
|
title: string;
|
|
30
16
|
anchors: Array<{
|
|
@@ -33,15 +19,15 @@ export interface KnowledgeDoc {
|
|
|
33
19
|
bytes: number;
|
|
34
20
|
}>;
|
|
35
21
|
}
|
|
36
|
-
|
|
22
|
+
export type KnowledgeOrigin = 'github' | 'cache' | 'local' | 'seed';
|
|
37
23
|
export interface KnowledgePack {
|
|
38
|
-
/** Версия формата — чтобы рантайм не пытался читать несовместимый снапшот. */
|
|
39
24
|
version: 1;
|
|
40
|
-
/** Когда собран (ISO). Показывается в cms_whoami. */
|
|
41
25
|
builtAt: string;
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
source: KnowledgeOrigin;
|
|
27
|
+
repo?: string;
|
|
28
|
+
ref?: string;
|
|
29
|
+
sha?: string;
|
|
30
|
+
warning?: string;
|
|
44
31
|
docs: KnowledgeDoc[];
|
|
45
32
|
chunks: KnowledgeChunk[];
|
|
46
33
|
}
|
|
47
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/knowledge/types.js
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { Config } from './config/config.js';
|
|
3
3
|
import { Session, type SharedDeps } from './session.js';
|
|
4
|
-
|
|
5
|
-
* Собирает разделяемые зависимости: каталог операций, индекс знаний, журнал.
|
|
6
|
-
* Вынесено отдельно, чтобы remote-режим строил их один раз на процесс,
|
|
7
|
-
* а не на каждую сессию.
|
|
8
|
-
*/
|
|
9
|
-
export declare const createSharedDeps: (config: Config) => SharedDeps;
|
|
10
|
-
/**
|
|
11
|
-
* Создаёт MCP-сервер с полным набором инструментов.
|
|
12
|
-
* Сессия передаётся геттером: в stdio-режиме она одна на процесс,
|
|
13
|
-
* в HTTP-режиме — своя на каждое подключение, и код инструментов от этого не зависит.
|
|
14
|
-
*/
|
|
4
|
+
export declare const createSharedDeps: (config: Config) => Promise<SharedDeps>;
|
|
15
5
|
export declare const createServer: (deps: SharedDeps, getSession: () => Session) => McpServer;
|
|
16
|
-
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import { AuditLog } from './api/audit.js';
|
|
3
4
|
import { OperationCatalog } from './api/catalog.js';
|
|
@@ -8,30 +9,33 @@ import { registerApiDiscovery } from './tools/api-discovery.js';
|
|
|
8
9
|
import { registerDocs } from './tools/docs.js';
|
|
9
10
|
import { registerGuide } from './tools/guide.js';
|
|
10
11
|
import { registerWhoami } from './tools/whoami.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
const SERVER_VERSION = (() => {
|
|
13
|
+
try {
|
|
14
|
+
const pkg = createRequire(import.meta.url)('../package.json');
|
|
15
|
+
return pkg.version ?? '0.0.0';
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '0.0.0';
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
export const createSharedDeps = async (config) => ({
|
|
19
22
|
config,
|
|
20
|
-
catalog: OperationCatalog.
|
|
21
|
-
knowledge: new KnowledgeIndex(resolveKnowledge({
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
catalog: await OperationCatalog.resolve(config),
|
|
24
|
+
knowledge: new KnowledgeIndex(await resolveKnowledge({
|
|
25
|
+
repo: config.knowledge.repo,
|
|
26
|
+
ref: config.knowledge.ref,
|
|
27
|
+
...(config.knowledge.path ? { path: config.knowledge.path } : {}),
|
|
28
|
+
cacheDir: config.cacheDir,
|
|
29
|
+
refreshTtlMs: config.knowledge.refreshTtlMs,
|
|
30
|
+
offline: config.knowledge.offline,
|
|
31
|
+
...(config.knowledge.token ? { token: config.knowledge.token } : {}),
|
|
32
|
+
timeoutMs: config.requestTimeoutMs,
|
|
24
33
|
})),
|
|
25
34
|
audit: new AuditLog({
|
|
26
35
|
...(config.auditPath ? { path: config.auditPath } : {}),
|
|
27
36
|
mode: config.mode,
|
|
28
37
|
}),
|
|
29
38
|
});
|
|
30
|
-
/**
|
|
31
|
-
* Создаёт MCP-сервер с полным набором инструментов.
|
|
32
|
-
* Сессия передаётся геттером: в stdio-режиме она одна на процесс,
|
|
33
|
-
* в HTTP-режиме — своя на каждое подключение, и код инструментов от этого не зависит.
|
|
34
|
-
*/
|
|
35
39
|
export const createServer = (deps, getSession) => {
|
|
36
40
|
const server = new McpServer({ name: 'oneentry-mcp-platform', version: SERVER_VERSION }, {
|
|
37
41
|
instructions: 'OneEntry CMS Admin API. Call cms_guide first, then cms_docs_read on "mcp/operating-rules" before any write. ' +
|
|
@@ -45,4 +49,3 @@ export const createServer = (deps, getSession) => {
|
|
|
45
49
|
registerApiCall(server, getSession);
|
|
46
50
|
return server;
|
|
47
51
|
};
|
|
48
|
-
//# sourceMappingURL=server.js.map
|
package/dist/session.d.ts
CHANGED
|
@@ -6,21 +6,12 @@ import { ConfirmStore } from './api/policy.js';
|
|
|
6
6
|
import type { Config } from './config/config.js';
|
|
7
7
|
import { KnowledgeIndex } from './knowledge/search.js';
|
|
8
8
|
import type { Operation } from './api/types.js';
|
|
9
|
-
/**
|
|
10
|
-
* Разделяемые между сессиями зависимости.
|
|
11
|
-
* Каталог и индекс знаний строятся один раз на процесс — они неизменяемы,
|
|
12
|
-
* и в remote-режиме держать их копию на сессию было бы чистой растратой.
|
|
13
|
-
*/
|
|
14
9
|
export interface SharedDeps {
|
|
15
10
|
config: Config;
|
|
16
11
|
catalog: OperationCatalog;
|
|
17
12
|
knowledge: KnowledgeIndex;
|
|
18
13
|
audit: AuditLog;
|
|
19
14
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Состояние одной MCP-сессии: свои учётные данные, свои токены,
|
|
22
|
-
* свои токены подтверждения. Именно эта изоляция делает remote-режим безопасным.
|
|
23
|
-
*/
|
|
24
15
|
export declare class Session {
|
|
25
16
|
readonly shared: SharedDeps;
|
|
26
17
|
readonly confirms: ConfirmStore;
|
|
@@ -29,15 +20,7 @@ export declare class Session {
|
|
|
29
20
|
private identityCache;
|
|
30
21
|
constructor(shared: SharedDeps, credentials?: Credentials);
|
|
31
22
|
get isConfigured(): boolean;
|
|
32
|
-
|
|
33
|
-
* Кто мы. Кэшируется: набор прав нужен почти каждому изменяющему вызову,
|
|
34
|
-
* а меняется он редко.
|
|
35
|
-
*/
|
|
23
|
+
accessToken(): Promise<string>;
|
|
36
24
|
identity(): Promise<AdminIdentity | undefined>;
|
|
37
|
-
/**
|
|
38
|
-
* Пытается показать, что именно будет изменено: ищет GET по тому же пути
|
|
39
|
-
* и читает объект. Без этого «подтверждение удаления» — подпись под пустым листом.
|
|
40
|
-
*/
|
|
41
25
|
describeTarget(operation: Operation, args: CallArgs): Promise<unknown>;
|
|
42
26
|
}
|
|
43
|
-
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { adminIdFromToken, credentialsFromConfig, TokenStore } from './api/auth.js';
|
|
2
2
|
import { AdminApiClient, buildUrl } from './api/client.js';
|
|
3
3
|
import { ConfirmStore } from './api/policy.js';
|
|
4
|
-
/**
|
|
5
|
-
* Состояние одной MCP-сессии: свои учётные данные, свои токены,
|
|
6
|
-
* свои токены подтверждения. Именно эта изоляция делает remote-режим безопасным.
|
|
7
|
-
*/
|
|
8
4
|
export class Session {
|
|
9
5
|
shared;
|
|
10
6
|
confirms = new ConfirmStore();
|
|
@@ -27,10 +23,9 @@ export class Session {
|
|
|
27
23
|
get isConfigured() {
|
|
28
24
|
return this.tokens.isConfigured;
|
|
29
25
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*/
|
|
26
|
+
async accessToken() {
|
|
27
|
+
return this.tokens.accessToken();
|
|
28
|
+
}
|
|
34
29
|
async identity() {
|
|
35
30
|
if (this.identityCache) {
|
|
36
31
|
return this.identityCache;
|
|
@@ -49,10 +44,6 @@ export class Session {
|
|
|
49
44
|
return undefined;
|
|
50
45
|
}
|
|
51
46
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Пытается показать, что именно будет изменено: ищет GET по тому же пути
|
|
54
|
-
* и читает объект. Без этого «подтверждение удаления» — подпись под пустым листом.
|
|
55
|
-
*/
|
|
56
47
|
async describeTarget(operation, args) {
|
|
57
48
|
const sibling = this.shared.catalog
|
|
58
49
|
.operations()
|
|
@@ -72,4 +63,3 @@ export class Session {
|
|
|
72
63
|
return result.ok ? result.body : undefined;
|
|
73
64
|
}
|
|
74
65
|
}
|
|
75
|
-
//# sourceMappingURL=session.js.map
|
package/dist/tools/api-call.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { Session } from '../session.js';
|
|
3
|
-
/** Регистрирует `cms_api_call`. */
|
|
4
3
|
export declare const registerApiCall: (server: McpServer, getSession: () => Session) => void;
|
|
5
|
-
//# sourceMappingURL=api-call.d.ts.map
|
package/dist/tools/api-call.js
CHANGED
|
@@ -4,9 +4,7 @@ import { buildUrl, normalizeBody, RequestBuildError } 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';
|
|
7
|
-
/** Ограничение на показ тела в плане dry-run. */
|
|
8
7
|
const PLAN_BODY_LIMIT = 2_000;
|
|
9
|
-
/** Регистрирует `cms_api_call`. */
|
|
10
8
|
export const registerApiCall = (server, getSession) => {
|
|
11
9
|
server.registerTool('cms_api_call', {
|
|
12
10
|
title: 'Call an Admin API operation',
|
|
@@ -42,11 +40,6 @@ export const registerApiCall = (server, getSession) => {
|
|
|
42
40
|
hint: 'Operation ids come from cms_api_search — do not construct them by hand.',
|
|
43
41
|
});
|
|
44
42
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Тело нормализуется до всего остального: и хэш аудита, и токен подтверждения
|
|
47
|
-
* привязываются именно к отправляемым аргументам, иначе подтверждение,
|
|
48
|
-
* выданное на dry-run, не совпало бы с реальным вызовом.
|
|
49
|
-
*/
|
|
50
43
|
const normalizedBody = normalizeBody(body);
|
|
51
44
|
const args = {
|
|
52
45
|
...(path ? { path } : {}),
|
|
@@ -60,24 +53,15 @@ export const registerApiCall = (server, getSession) => {
|
|
|
60
53
|
path: operation.path,
|
|
61
54
|
argsHash,
|
|
62
55
|
};
|
|
63
|
-
/**
|
|
64
|
-
* Уровень доступа проверяется до аутентификации: иначе отказ read-only режима
|
|
65
|
-
* всё равно сходил бы в сеть за токеном, и обещание «no request was sent» было бы ложью.
|
|
66
|
-
*/
|
|
67
56
|
const levelDenial = checkLevel(operation, config.allow);
|
|
68
57
|
if (levelDenial) {
|
|
69
58
|
audit.record({ ...auditBase, outcome: 'denied' });
|
|
70
59
|
return errorResult(levelDenial.reason, { opId, policy: config.allow, risk: operation.risk });
|
|
71
60
|
}
|
|
72
|
-
/** Личность нужна только там, где операция объявляет требуемое право. */
|
|
73
61
|
const identity = operation.permission ? await session.identity() : undefined;
|
|
74
62
|
if (identity) {
|
|
75
63
|
Object.assign(auditBase, { adminId: identity.id });
|
|
76
64
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Негодный токен — отдельная ошибка, а не «просто нет подтверждения»:
|
|
79
|
-
* иначе агент получал бы в ответ новый токен и не понимал, что предыдущий не подошёл.
|
|
80
|
-
*/
|
|
81
65
|
const confirmValid = confirm !== undefined && session.confirms.verify(confirm, opId, args);
|
|
82
66
|
if (confirm !== undefined && !confirmValid) {
|
|
83
67
|
return errorResult('Confirm token is expired, already used, or does not match these arguments. Re-run with dryRun: true to get a fresh one.', { opId });
|
|
@@ -94,7 +78,6 @@ export const registerApiCall = (server, getSession) => {
|
|
|
94
78
|
}
|
|
95
79
|
return errorResult(decision.reason, { opId, policy: config.allow, risk: operation.risk });
|
|
96
80
|
}
|
|
97
|
-
/** Разрешение по уровню и правам получено — можно строить сам запрос. */
|
|
98
81
|
let url;
|
|
99
82
|
try {
|
|
100
83
|
url = buildUrl(config.baseUrl, operation, args);
|
|
@@ -134,10 +117,6 @@ export const registerApiCall = (server, getSession) => {
|
|
|
134
117
|
target: summarizeTarget(target),
|
|
135
118
|
});
|
|
136
119
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Токен гасится только здесь, перед самой отправкой: если бы он расходовался
|
|
139
|
-
* при проверке политики, отказ по другой причине заставлял бы подтверждать заново.
|
|
140
|
-
*/
|
|
141
120
|
if (confirm !== undefined && !session.confirms.consume(confirm, opId, args)) {
|
|
142
121
|
return errorResult('Confirm token was consumed concurrently. Re-run with dryRun: true to get a fresh one.', { opId });
|
|
143
122
|
}
|
|
@@ -154,7 +133,6 @@ export const registerApiCall = (server, getSession) => {
|
|
|
154
133
|
opId,
|
|
155
134
|
status: result.error.status,
|
|
156
135
|
...(result.error.hint ? { hint: result.error.hint } : {}),
|
|
157
|
-
docs: operation.docLinks,
|
|
158
136
|
});
|
|
159
137
|
}
|
|
160
138
|
const shaped = shapeResponse(result.body, config.maxResponseBytes);
|
|
@@ -166,4 +144,3 @@ export const registerApiCall = (server, getSession) => {
|
|
|
166
144
|
});
|
|
167
145
|
});
|
|
168
146
|
};
|
|
169
|
-
//# sourceMappingURL=api-call.js.map
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { SharedDeps } from '../session.js';
|
|
3
|
-
/** Регистрирует `cms_api_search` и `cms_api_describe`. */
|
|
4
3
|
export declare const registerApiDiscovery: (server: McpServer, deps: SharedDeps) => void;
|
|
5
|
-
//# sourceMappingURL=api-discovery.d.ts.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { errorResult, jsonResult } from './result.js';
|
|
3
|
-
/** Регистрирует `cms_api_search` и `cms_api_describe`. */
|
|
4
3
|
export const registerApiDiscovery = (server, deps) => {
|
|
5
4
|
const { catalog } = deps;
|
|
6
5
|
server.registerTool('cms_api_search', {
|
|
@@ -36,7 +35,7 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
36
35
|
});
|
|
37
36
|
server.registerTool('cms_api_describe', {
|
|
38
37
|
title: 'Describe an Admin API operation',
|
|
39
|
-
description: 'Full detail for one operation: path and query parameters, request-body schema, required permission, risk level, and
|
|
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.',
|
|
40
39
|
inputSchema: {
|
|
41
40
|
opId: z.string().min(1).describe('Operation id from cms_api_search, e.g. "AdminPagesController_findAllRoot".'),
|
|
42
41
|
},
|
|
@@ -70,11 +69,9 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
70
69
|
body: operation.body ?? null,
|
|
71
70
|
looseFields: loose,
|
|
72
71
|
responseSummary: operation.responseSummary ?? null,
|
|
73
|
-
docs: operation.docLinks,
|
|
74
72
|
next: operation.risk === 'read'
|
|
75
73
|
? 'Call it with cms_api_call { opId, path, query }.'
|
|
76
|
-
: '
|
|
74
|
+
: 'Search the knowledge base with cms_docs_search for this entity, then call cms_api_call with dryRun: true first.',
|
|
77
75
|
});
|
|
78
76
|
});
|
|
79
77
|
};
|
|
80
|
-
//# sourceMappingURL=api-discovery.js.map
|
package/dist/tools/docs.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { SharedDeps } from '../session.js';
|
|
3
|
-
/** Регистрирует `cms_docs_search` и `cms_docs_read`. */
|
|
4
3
|
export declare const registerDocs: (server: McpServer, deps: SharedDeps) => void;
|
|
5
|
-
//# sourceMappingURL=docs.d.ts.map
|
package/dist/tools/docs.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { errorResult, jsonResult } from './result.js';
|
|
3
|
-
/** Предел размера одного раздела, отдаваемого модели. */
|
|
4
3
|
const MAX_SECTION_BYTES = 12_288;
|
|
5
|
-
/** Регистрирует `cms_docs_search` и `cms_docs_read`. */
|
|
6
4
|
export const registerDocs = (server, deps) => {
|
|
7
5
|
const { knowledge } = deps;
|
|
8
6
|
server.registerTool('cms_docs_search', {
|
|
@@ -37,7 +35,7 @@ export const registerDocs = (server, deps) => {
|
|
|
37
35
|
title: 'Read a CMS knowledge section',
|
|
38
36
|
description: 'Read one section of a knowledge document. Omit the anchor to get the document preamble plus its section list, then page through the sections you need.',
|
|
39
37
|
inputSchema: {
|
|
40
|
-
docId: z.string().min(1).describe('Document id, e.g. "
|
|
38
|
+
docId: z.string().min(1).describe('Document id, e.g. "mcp/docs/api/orders" or "mcp/operating-rules".'),
|
|
41
39
|
anchor: z.string().optional().describe('Section anchor from cms_docs_search; omit for the preamble.'),
|
|
42
40
|
},
|
|
43
41
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
@@ -73,7 +71,6 @@ export const registerDocs = (server, deps) => {
|
|
|
73
71
|
sections: doc.anchors.map((a) => ({ anchor: a.anchor, title: a.title, bytes: a.bytes })),
|
|
74
72
|
});
|
|
75
73
|
});
|
|
76
|
-
/** Ресурсы дублируют два самых нужных документа, чтобы клиент мог их закрепить. */
|
|
77
74
|
server.registerResource('operating-rules', 'oneentry://knowledge/mcp/operating-rules', {
|
|
78
75
|
title: 'OneEntry CMS operating rules',
|
|
79
76
|
description: 'The payload and safety rules an agent must follow before writing to this CMS.',
|
|
@@ -110,4 +107,3 @@ export const registerDocs = (server, deps) => {
|
|
|
110
107
|
],
|
|
111
108
|
}));
|
|
112
109
|
};
|
|
113
|
-
//# sourceMappingURL=docs.js.map
|
package/dist/tools/guide.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { SharedDeps } from '../session.js';
|
|
3
|
-
/**
|
|
4
|
-
* Собирает краткое руководство: режим, политика, карта тегов и порядок работы.
|
|
5
|
-
* Генерируется, а не хранится текстом, потому что половина содержимого —
|
|
6
|
-
* фактическое состояние сервера, и расхождение здесь дороже дублирования.
|
|
7
|
-
*/
|
|
8
3
|
export declare const renderGuide: (deps: SharedDeps) => string;
|
|
9
|
-
/** Регистрирует `cms_guide`. */
|
|
10
4
|
export declare const registerGuide: (server: McpServer, deps: SharedDeps) => void;
|
|
11
|
-
//# sourceMappingURL=guide.d.ts.map
|
package/dist/tools/guide.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { textResult } from './result.js';
|
|
2
|
-
/** Сколько тегов показывать в карте API. */
|
|
3
2
|
const TAG_LIMIT = 24;
|
|
4
|
-
/**
|
|
5
|
-
* Собирает краткое руководство: режим, политика, карта тегов и порядок работы.
|
|
6
|
-
* Генерируется, а не хранится текстом, потому что половина содержимого —
|
|
7
|
-
* фактическое состояние сервера, и расхождение здесь дороже дублирования.
|
|
8
|
-
*/
|
|
9
3
|
export const renderGuide = (deps) => {
|
|
10
4
|
const { config, catalog, knowledge } = deps;
|
|
11
5
|
const counts = new Map();
|
|
@@ -22,12 +16,19 @@ export const renderGuide = (deps) => {
|
|
|
22
16
|
: config.allow === 'write'
|
|
23
17
|
? 'writes allowed; DELETE and the permanently gated paths still need a confirm token'
|
|
24
18
|
: 'writes and deletes allowed; deletes and the permanently gated paths still need a confirm token';
|
|
19
|
+
const pack = knowledge.pack;
|
|
20
|
+
const knowledgeOrigin = pack.source === 'seed'
|
|
21
|
+
? 'bundled operating rules only'
|
|
22
|
+
: pack.source === 'local'
|
|
23
|
+
? `local clone ${pack.repo ?? ''}`.trim()
|
|
24
|
+
: `${pack.repo ?? ''}@${pack.ref ?? ''} (${pack.sha?.slice(0, 12) ?? '?'}, ${pack.source})`;
|
|
25
25
|
return [
|
|
26
26
|
'# OneEntry CMS Admin API — agent guide',
|
|
27
27
|
'',
|
|
28
28
|
`Mode: **${config.mode}** · Base URL: \`${config.baseUrl}\` · Policy: ${allowLine}.`,
|
|
29
|
-
`Catalog: ${String(catalog.operations().length)} operations (swagger ${catalog.catalog.swaggerHash}).`,
|
|
30
|
-
`Knowledge: ${String(knowledge.docs().length)} docs / ${String(
|
|
29
|
+
`Catalog: ${String(catalog.operations().length)} operations (swagger ${catalog.catalog.swaggerHash || 'unavailable'}).`,
|
|
30
|
+
`Knowledge: ${String(knowledge.docs().length)} docs / ${String(pack.chunks.length)} sections, from ${knowledgeOrigin}.`,
|
|
31
|
+
...(pack.warning ? ['', `> **Knowledge is degraded.** ${pack.warning}`] : []),
|
|
31
32
|
'',
|
|
32
33
|
'## How to work here',
|
|
33
34
|
'',
|
|
@@ -35,7 +36,8 @@ export const renderGuide = (deps) => {
|
|
|
35
36
|
' payload rules (locale-keyed `localizeInfos`, attribute-set-driven `attributesSets`,',
|
|
36
37
|
' lexorank vs numeric `position`, eventual consistency) that make the difference between',
|
|
37
38
|
' a 400 and a correct call.',
|
|
38
|
-
'2. `cms_docs_search` — find the reference doc for the entity you are touching
|
|
39
|
+
'2. `cms_docs_search` — find the reference doc for the entity you are touching;',
|
|
40
|
+
' `mcp/docs/server/doc-map` lists every document with a one-line "read this when".',
|
|
39
41
|
'3. `cms_api_search` → `cms_api_describe` — get the real operation and its payload shape.',
|
|
40
42
|
'4. `cms_api_call` — `dryRun: true` first for anything that mutates.',
|
|
41
43
|
'',
|
|
@@ -56,7 +58,6 @@ export const renderGuide = (deps) => {
|
|
|
56
58
|
: []),
|
|
57
59
|
].join('\n');
|
|
58
60
|
};
|
|
59
|
-
/** Регистрирует `cms_guide`. */
|
|
60
61
|
export const registerGuide = (server, deps) => {
|
|
61
62
|
server.registerTool('cms_guide', {
|
|
62
63
|
title: 'CMS guide',
|
|
@@ -64,4 +65,3 @@ export const registerGuide = (server, deps) => {
|
|
|
64
65
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
65
66
|
}, () => textResult(renderGuide(deps)));
|
|
66
67
|
};
|
|
67
|
-
//# sourceMappingURL=guide.js.map
|
package/dist/tools/result.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Форма результата вызова инструмента MCP. */
|
|
2
1
|
export interface ToolResult {
|
|
3
2
|
content: Array<{
|
|
4
3
|
type: 'text';
|
|
@@ -7,13 +6,6 @@ export interface ToolResult {
|
|
|
7
6
|
isError?: boolean;
|
|
8
7
|
[key: string]: unknown;
|
|
9
8
|
}
|
|
10
|
-
/** Успешный результат-JSON. */
|
|
11
9
|
export declare const jsonResult: (payload: unknown) => ToolResult;
|
|
12
|
-
/** Успешный результат-текст (для человекочитаемых руководств). */
|
|
13
10
|
export declare const textResult: (text: string) => ToolResult;
|
|
14
|
-
/**
|
|
15
|
-
* Ошибка инструмента. Возвращаем её как isError, а не бросаем исключение:
|
|
16
|
-
* модель должна увидеть текст и исправиться, а не получить обрыв сессии.
|
|
17
|
-
*/
|
|
18
11
|
export declare const errorResult: (message: string, extra?: Record<string, unknown>) => ToolResult;
|
|
19
|
-
//# sourceMappingURL=result.d.ts.map
|
package/dist/tools/result.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
/** Успешный результат-JSON. */
|
|
2
1
|
export const jsonResult = (payload) => ({
|
|
3
2
|
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
4
3
|
});
|
|
5
|
-
/** Успешный результат-текст (для человекочитаемых руководств). */
|
|
6
4
|
export const textResult = (text) => ({
|
|
7
5
|
content: [{ type: 'text', text }],
|
|
8
6
|
});
|
|
9
|
-
/**
|
|
10
|
-
* Ошибка инструмента. Возвращаем её как isError, а не бросаем исключение:
|
|
11
|
-
* модель должна увидеть текст и исправиться, а не получить обрыв сессии.
|
|
12
|
-
*/
|
|
13
7
|
export const errorResult = (message, extra) => ({
|
|
14
8
|
content: [
|
|
15
9
|
{ type: 'text', text: JSON.stringify({ error: message, ...(extra ?? {}) }, null, 2) },
|
|
16
10
|
],
|
|
17
11
|
isError: true,
|
|
18
12
|
});
|
|
19
|
-
//# sourceMappingURL=result.js.map
|
package/dist/tools/whoami.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { Session } from '../session.js';
|
|
3
|
-
/** Регистрирует `cms_whoami` — состояние сервера и подключённого админа. */
|
|
4
3
|
export declare const registerWhoami: (server: McpServer, getSession: () => Session) => void;
|
|
5
|
-
//# sourceMappingURL=whoami.d.ts.map
|
package/dist/tools/whoami.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsonResult } from './result.js';
|
|
2
|
-
/** Регистрирует `cms_whoami` — состояние сервера и подключённого админа. */
|
|
3
2
|
export const registerWhoami = (server, getSession) => {
|
|
4
3
|
server.registerTool('cms_whoami', {
|
|
5
4
|
title: 'Who am I',
|
|
@@ -19,7 +18,7 @@ export const registerWhoami = (server, getSession) => {
|
|
|
19
18
|
: {
|
|
20
19
|
admin: null,
|
|
21
20
|
hint: session.isConfigured
|
|
22
|
-
? 'Credentials are configured but authentication failed — check ONEENTRY_CMS_LOGIN / ONEENTRY_CMS_PASSWORD and that the
|
|
21
|
+
? 'Credentials are configured but authentication failed — check ONEENTRY_CMS_LOGIN / ONEENTRY_CMS_PASSWORD and that the instance is reachable.'
|
|
23
22
|
: 'No credentials configured. Set ONEENTRY_CMS_LOGIN + ONEENTRY_CMS_PASSWORD (or ONEENTRY_CMS_TOKEN).',
|
|
24
23
|
}),
|
|
25
24
|
catalog: {
|
|
@@ -31,12 +30,15 @@ export const registerWhoami = (server, getSession) => {
|
|
|
31
30
|
},
|
|
32
31
|
knowledge: {
|
|
33
32
|
source: knowledge.pack.source,
|
|
33
|
+
...(knowledge.pack.repo ? { repo: knowledge.pack.repo } : {}),
|
|
34
|
+
...(knowledge.pack.ref ? { ref: knowledge.pack.ref } : {}),
|
|
35
|
+
...(knowledge.pack.sha ? { commit: knowledge.pack.sha.slice(0, 12) } : {}),
|
|
34
36
|
builtAt: knowledge.pack.builtAt,
|
|
35
37
|
docs: knowledge.docs().length,
|
|
36
38
|
sections: knowledge.pack.chunks.length,
|
|
39
|
+
...(knowledge.pack.warning ? { warning: knowledge.pack.warning } : {}),
|
|
37
40
|
},
|
|
38
41
|
audit: config.auditPath ?? null,
|
|
39
42
|
});
|
|
40
43
|
});
|
|
41
44
|
};
|
|
42
|
-
//# sourceMappingURL=whoami.js.map
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { Config } from '../config/config.js';
|
|
2
|
-
/**
|
|
3
|
-
* Запускает сервер поверх Streamable HTTP — remote-режим.
|
|
4
|
-
* На каждое MCP-подключение создаётся своя `Session` со своими токенами
|
|
5
|
-
* и своими подтверждениями; общий каталог и знания строятся один раз.
|
|
6
|
-
*/
|
|
7
2
|
export declare const runHttp: (config: Config) => Promise<{
|
|
8
3
|
close: () => Promise<void>;
|
|
9
4
|
}>;
|
|
10
|
-
//# sourceMappingURL=http.d.ts.map
|