feat: mcpctl v0.0.1 — first public release
Some checks are pending
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (push) Waiting to run
CI / build (push) Blocked by required conditions
CI / package (push) Blocked by required conditions

Comprehensive MCP server management with kubectl-style CLI.

Key features in this release:
- Declarative YAML apply/get round-trip with project cloning support
- Gated sessions with prompt intelligence for Claude
- Interactive MCP console with traffic inspector
- Persistent STDIO connections for containerized servers
- RBAC with name-scoped bindings
- Shell completions (fish + bash) auto-generated
- Rate-limit retry with exponential backoff in apply
- Project-scoped prompt management
- Credential scrubbing from git history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-27 17:05:05 +00:00
parent 414a8d3774
commit 69867bd47a
65 changed files with 5710 additions and 695 deletions

View File

@@ -0,0 +1,20 @@
# Docker image for MrMartiniMo/docmost-mcp (TypeScript STDIO MCP server)
# Not published to npm, so we clone + build from source.
# Includes patches for list_pages pagination and search response handling.
FROM node:20-slim
WORKDIR /mcp
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 https://github.com/MrMartiniMo/docmost-mcp.git . \
&& npm install \
&& rm -rf .git
# Apply our fixes before building
COPY deploy/docmost-mcp-fixes.patch /tmp/fixes.patch
RUN git init && git add -A && git apply /tmp/fixes.patch && rm -rf .git /tmp/fixes.patch
RUN npm run build
ENTRYPOINT ["node", "build/index.js"]

View File

@@ -0,0 +1,106 @@
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<string, any> = { 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<string, string | string[]>) => {
+ 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);