feat(mcp): getTree — иерархия пространства/поддерева одним вызовом (#443, часть 2/3)
Раньше единственный способ получить дерево — listPages tree:true: BFS по sidebar-эндпоинту, десятки-сотни HTTP-вызовов и молчаливая потеря страниц (#442). Бэкенд форка уже отдаёт всё одним POST /pages/tree (getSidebarPagesTree, merged #442/#451), так что это MCP-сторона + общий реестр. Только чтение. - lib/tree.ts: buildPageTree(nodes) аддитивно расширен до buildPageTree(nodes, options?) с {shape?: "lean"|"getTree"; maxDepth?}. Дефолт/{} — байт-идентичный lean {id,slugId,title,children?} (существующие вызыватели listPages tree:true и subtree-BFS не тронуты, есть регрессионный тест на точный набор полей). shape:"getTree" проецирует {pageId, title, children?, hasChildren?} (id→pageId; slugId/icon/position/parentPageId не утекают ни на одной глубине). - client.ts: getTree(spaceId, rootPageId?, maxDepth?) — тот же код-путь, что listPages tree:true: один enumerateSpacePages(spaceId, rootPageId) (единичный /pages/tree + cursor-BFS фолбэк для stock upstream) → buildPageTree(pages, {shape:"getTree", maxDepth}). listPages tree:true помечен DEPRECATED в JSDoc (BFS-фолбэк на месте, поведение не тронуто). - maxDepth/hasChildren: полное дерево строится, потом обрезается. Корни = глубина 1; maxDepth:1 → только корни; hasChildren:true ТОЛЬКО на срезанном узле с серверным hasChildren (source of truth), опущен у листьев и раскрытых узлов. Схема тула min(1) отбраковывает 0/отрицательные. - tool-specs.ts: shared-спек getTree (оба хоста через цикл реестра), DocmostClientLike Pick += getTree, listPages desc/catalogLine — про депрекацию. server-instructions.ts: getTree:"READ" + routing-проза. Loader DocmostClientMethod += getTree, compile-time client-call contract += getTree. Циклы/self-ref безопасны: project рекурсирует только от корней, циклический компонент недостижим из корней (нет бесконечной рекурсии). Orphan (родитель отфильтрован пермишенами) всплывает как корень. Тесты: tree.test.mjs +9 (nesting+порядок+no-leak, maxDepth:1/2 + hasChildren, orphan→root, seeded rootPageId, ≤0/нефинитный = без среза, lean-байт-идентичность), tool-specs.test.mjs (схема getTree + депрекация listPages). mcp node --test 718/718, tsc чисто; server jest (shared-tool-specs.contract + ai-chat) 263/263. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,7 @@ export type DocmostClientLike = Pick<
|
||||
| 'getSpaces'
|
||||
| 'listShares'
|
||||
| 'listPages'
|
||||
| 'getTree'
|
||||
| 'getPage'
|
||||
| 'getPageJson'
|
||||
| 'getOutline'
|
||||
@@ -917,11 +918,13 @@ export const SHARED_TOOL_SPECS = {
|
||||
description:
|
||||
'List the most recent pages (ordered by updatedAt, descending), ' +
|
||||
'optionally scoped to a single space. Returns a bounded list (default ' +
|
||||
'50, max 100) — use search for lookups in large spaces. Pass tree:true ' +
|
||||
"(with spaceId) to instead get the space's full page hierarchy as a " +
|
||||
'nested tree.',
|
||||
'50, max 100) — use search for lookups in large spaces. tree:true (with ' +
|
||||
"spaceId) returns the space's full page hierarchy as a nested tree, but " +
|
||||
'is DEPRECATED — use getTree instead (leaner nodes, plus rootPageId / ' +
|
||||
'maxDepth).',
|
||||
tier: 'core',
|
||||
catalogLine: "listPages — list recent pages, or a space's full page tree.",
|
||||
catalogLine:
|
||||
"listPages — list recent pages (tree:true is deprecated; use getTree for the hierarchy).",
|
||||
buildShape: (z) => ({
|
||||
spaceId: z
|
||||
.string()
|
||||
@@ -954,6 +957,50 @@ export const SHARED_TOOL_SPECS = {
|
||||
),
|
||||
},
|
||||
|
||||
getTree: {
|
||||
mcpName: 'getTree',
|
||||
inAppKey: 'getTree',
|
||||
description:
|
||||
"Get a space's page hierarchy (or one subtree) as a nested tree in a " +
|
||||
'SINGLE request — completely and without loss. Each node is ' +
|
||||
'`{ pageId, title, children? }`; children are ordered as in the sidebar. ' +
|
||||
'Pass rootPageId to return only that page and its descendants (exactly ' +
|
||||
'one root). Pass maxDepth to trim depth and save tokens (root nodes are ' +
|
||||
'depth 1, so maxDepth:1 returns only the roots); a node whose children ' +
|
||||
'were trimmed carries `hasChildren:true` so you can descend later with ' +
|
||||
'getTree(rootPageId=that page). Prefer this over listPages tree:true.',
|
||||
tier: 'core',
|
||||
catalogLine:
|
||||
"getTree — a space's page hierarchy (or a subtree) as a nested tree in one request.",
|
||||
buildShape: (z) => ({
|
||||
spaceId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('The id of the space whose page tree to return.'),
|
||||
rootPageId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional page id: return only this page and its descendants (one root).',
|
||||
),
|
||||
maxDepth: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional depth cap (roots are depth 1). maxDepth:1 returns only the ' +
|
||||
'roots; trimmed nodes carry hasChildren:true.',
|
||||
),
|
||||
}),
|
||||
execute: (client, { spaceId, rootPageId, maxDepth }) =>
|
||||
client.getTree(
|
||||
spaceId as string,
|
||||
rootPageId as string | undefined,
|
||||
maxDepth as number | undefined,
|
||||
),
|
||||
},
|
||||
|
||||
createPage: {
|
||||
mcpName: 'createPage',
|
||||
inAppKey: 'createPage',
|
||||
|
||||
Reference in New Issue
Block a user