feat(mcp): add hierarchical tree mode to list_pages
list_pages gains an opt-in `tree` parameter on both surfaces (the
@docmost/mcp server tool and the AI-chat agent tool), which share the
same DocmostClient.listPages. Default behavior (recent-by-updatedAt flat
list) is unchanged.
- client.ts: listPages(spaceId?, limit=50, tree=false); when tree is
true it requires spaceId (throws a specific error otherwise), walks the
sidebar tree via the existing bounded/cycle-safe enumerateSpacePages,
and returns a nested tree; limit is ignored in tree mode.
- lib/tree.ts: new pure buildPageTree() — lean nodes { id, slugId, title,
children? }, children sorted by position (code-unit order), orphans
promoted to roots, cycle-safe.
- index.ts + ai-chat-tools.service.ts: expose `tree` in the tool schemas
and descriptions; docmost-client.loader.ts: mirror the new signature.
- tests: add packages/mcp/test/unit/tree.test.mjs (nesting, ordering,
lean shape, orphan promotion, cycle/self-reference safety).
- rebuild @docmost/mcp (build/ is tracked and loaded at runtime).
This commit is contained in:
@@ -9,6 +9,7 @@ import WebSocket from "ws";
|
||||
import { convertProseMirrorToMarkdown } from "./lib/markdown-converter.js";
|
||||
import { updatePageContentRealtime, replacePageContent, markdownToProseMirror, mutatePageContent, buildCollabWsUrl, assertYjsEncodable, } from "./lib/collaboration.js";
|
||||
import { docmostExtensions } from "./lib/docmost-schema.js";
|
||||
import { buildPageTree } from "./lib/tree.js";
|
||||
import { serializeDocmostMarkdown, parseDocmostMarkdown, } from "./lib/markdown-document.js";
|
||||
import { replaceNodeById, deleteNodeById, insertNodeRelative, buildOutline, getNodeByRef, readTable, insertTableRow, deleteTableRow, updateTableCell, } from "./lib/node-ops.js";
|
||||
import { withPageLock } from "./lib/page-lock.js";
|
||||
@@ -440,12 +441,29 @@ export class DocmostClient {
|
||||
return spaces.map((space) => filterSpace(space));
|
||||
}
|
||||
/**
|
||||
* List most recent pages (bounded). Fetching the whole space can exceed
|
||||
* MCP response/time limits on large instances, so a single bounded page
|
||||
* of results is returned (default 50, max 100).
|
||||
* List pages in one of two modes.
|
||||
*
|
||||
* Default (`tree` false): most recent pages by updatedAt (descending),
|
||||
* bounded. Fetching the whole space can exceed MCP response/time limits on
|
||||
* large instances, so a single bounded page of results is returned (default
|
||||
* 50, max 100) via the `/pages/recent` feed.
|
||||
*
|
||||
* Tree (`tree` true): the space's FULL page hierarchy as a nested tree (each
|
||||
* node has a `children` array). This mode REQUIRES `spaceId` (a page tree is
|
||||
* scoped to one space) and IGNORES `limit` — the whole hierarchy is returned.
|
||||
* It walks the sidebar tree via `enumerateSpacePages`, which performs N
|
||||
* sidebar requests and is bounded by that method's 10000-node cap (and skips
|
||||
* soft-deleted pages server-side).
|
||||
*/
|
||||
async listPages(spaceId, limit = 50) {
|
||||
async listPages(spaceId, limit = 50, tree = false) {
|
||||
await this.ensureAuthenticated();
|
||||
if (tree) {
|
||||
if (!spaceId) {
|
||||
throw new Error("list_pages: tree mode requires a spaceId (a page tree is scoped to one space). Pass spaceId, or omit tree to get the recent-pages list.");
|
||||
}
|
||||
const nodes = await this.enumerateSpacePages(spaceId);
|
||||
return buildPageTree(nodes);
|
||||
}
|
||||
const clampedLimit = Math.max(1, Math.min(100, limit));
|
||||
const payload = { limit: clampedLimit, page: 1 };
|
||||
if (spaceId)
|
||||
|
||||
Reference in New Issue
Block a user