feat: mcpctl v0.0.1 — first public release
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:
20
deploy/Dockerfile.docmost-mcp
Normal file
20
deploy/Dockerfile.docmost-mcp
Normal 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"]
|
||||
106
deploy/docmost-mcp-fixes.patch
Normal file
106
deploy/docmost-mcp-fixes.patch
Normal 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);
|
||||
Reference in New Issue
Block a user