diff --git a/src/index.ts b/src/index.ts index 83c251d..852ee0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import FormData from "form-data"; import axios, { AxiosInstance } from "axios"; @@ -130,10 +130,18 @@ class DocmostClient { return groups.map((group) => filterGroup(group)); } - async listPages(spaceId?: string) { - const payload = spaceId ? { spaceId } : {}; - const pages = await this.paginateAll("/pages/recent", payload); - return pages.map((page) => filterPage(page)); + async listPages(spaceId?: string, page: number = 1, limit: number = 50) { + await this.ensureAuthenticated(); + const clampedLimit = Math.max(1, Math.min(100, limit)); + const payload: Record = { page, limit: clampedLimit }; + if (spaceId) payload.spaceId = spaceId; + const response = await this.client.post("/pages/recent", payload); + const data = response.data; + const items = data.data?.items || data.items || []; + return { + pages: items.map((p: any) => filterPage(p)), + meta: data.data?.meta || data.meta || {}, + }; } async listSidebarPages(spaceId: string, pageId: string) { @@ -283,8 +291,9 @@ class DocmostClient { spaceId, }); - // Filter search results (data is directly an array) - const items = response.data?.data || []; + // Handle both array and {items: [...]} response formats + const rawData = response.data?.data; + const items = Array.isArray(rawData) ? rawData : (rawData?.items || []); const filteredItems = items.map((item: any) => filterSearchResult(item)); return { @@ -384,13 +393,15 @@ server.registerTool( server.registerTool( "list_pages", { - description: "List pages in a space ordered by updatedAt (descending).", + description: "List pages in a space ordered by updatedAt (descending). Returns one page of results.", inputSchema: { spaceId: z.string().optional(), + page: z.number().optional().describe("Page number (default: 1)"), + limit: z.number().optional().describe("Items per page, 1-100 (default: 50)"), }, }, - async ({ spaceId }) => { - const result = await docmostClient.listPages(spaceId); + async ({ spaceId, page, limit }) => { + const result = await docmostClient.listPages(spaceId, page, limit); return jsonContent(result); }, ); @@ -544,6 +555,41 @@ server.registerTool( }, ); +// Resource template: docmost://pages/{pageId} +// Allows MCP clients to read page content as resources +server.resource( + "page", + new ResourceTemplate("docmost://pages/{pageId}", { + list: async () => { + // List recent pages as browsable resources + try { + const result = await docmostClient.listPages(undefined, 1, 100); + return result.pages.map((page: any) => ({ + uri: `docmost://pages/${page.id}`, + name: page.title || page.id, + mimeType: "text/markdown", + })); + } catch { + return []; + } + }, + }), + { description: "A Docmost wiki page", mimeType: "text/markdown" }, + async (uri: URL, variables: Record) => { + const pageId = Array.isArray(variables.pageId) ? variables.pageId[0]! : variables.pageId!; + const page = await docmostClient.getPage(pageId); + return { + contents: [ + { + uri: uri.href, + text: page.data.content || `# ${page.data.title || "Untitled"}\n\n(No content)`, + mimeType: "text/markdown", + }, + ], + }; + }, +); + async function run() { const transport = new StdioServerTransport(); await server.connect(transport);