mdgen-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/client.js +95 -0
- package/dist/index.js +93 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# mdgen-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [mdgen](https://mdgen.app) — read and write your mdgen documents from
|
|
4
|
+
Claude Desktop, Claude Code, Codex CLI, and other MCP clients, using **your own**
|
|
5
|
+
account. The AI runs on your subscription; mdgen never pays for generation.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Exposes your saved mdgen documents (available to logged-in users) as MCP tools:
|
|
10
|
+
|
|
11
|
+
| Tool | Description |
|
|
12
|
+
|------|-------------|
|
|
13
|
+
| `list_documents` | List your documents (id, title, mode, updatedAt) |
|
|
14
|
+
| `read_document` | Get a document's full Markdown by id |
|
|
15
|
+
| `create_document` | Create a new document (`markdown`, `title?`, `mode?`) |
|
|
16
|
+
| `update_document` | Update a document (only provided fields change) |
|
|
17
|
+
| `delete_document` | Delete a document by id |
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
1. A mdgen account (sign in at https://mdgen.app).
|
|
22
|
+
2. A **personal access token (PAT)**: issue one from mdgen while logged in
|
|
23
|
+
(Menu → API tokens). Copy it — it is shown only once.
|
|
24
|
+
|
|
25
|
+
## Environment variables
|
|
26
|
+
|
|
27
|
+
| Variable | Required | Default | Description |
|
|
28
|
+
|----------|----------|---------|-------------|
|
|
29
|
+
| `MDGEN_TOKEN` | ✅ | — | Your mdgen personal access token |
|
|
30
|
+
| `MDGEN_API_URL` | | `https://api.mdgen.app` | mdgen API base URL |
|
|
31
|
+
|
|
32
|
+
## Client setup
|
|
33
|
+
|
|
34
|
+
### Claude Desktop
|
|
35
|
+
|
|
36
|
+
`claude_desktop_config.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"mdgen": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "mdgen-mcp"],
|
|
44
|
+
"env": {
|
|
45
|
+
"MDGEN_TOKEN": "<your token>"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Claude Code
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
claude mcp add mdgen --env MDGEN_TOKEN=<your token> -- npx -y mdgen-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Codex CLI
|
|
59
|
+
|
|
60
|
+
`~/.codex/config.toml`:
|
|
61
|
+
|
|
62
|
+
```toml
|
|
63
|
+
[mcp_servers.mdgen]
|
|
64
|
+
command = "npx"
|
|
65
|
+
args = ["-y", "mdgen-mcp"]
|
|
66
|
+
env = { MDGEN_TOKEN = "<your token>" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Security
|
|
70
|
+
|
|
71
|
+
- Your PAT is passed via your client's `env` and never leaves your machine except as a
|
|
72
|
+
`Authorization: Bearer` header to the mdgen API. It is **not** stored in this package.
|
|
73
|
+
- The PAT grants access only to **your** documents. Revoke it anytime from mdgen.
|
|
74
|
+
- Treat the token like a password. Do not commit it.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pnpm install
|
|
80
|
+
pnpm dev # run from source (tsx)
|
|
81
|
+
pnpm build # emit dist/
|
|
82
|
+
pnpm test # unit tests
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Related: mdgen issue #151.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// mdgen backend(documents API)を PAT(Bearer)で叩く薄いクライアント。
|
|
2
|
+
// 認証・データ分離は API 側(#151 Phase 1)が担う。ここは HTTP 呼び出しとエラー整形のみ。
|
|
3
|
+
/** API エラー。status と人間可読メッセージを持つ。 */
|
|
4
|
+
export class MdgenApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
constructor(status, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "MdgenApiError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function humanMessage(status, detail, retryAfter) {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case 401:
|
|
15
|
+
return "Authentication failed. Check that MDGEN_TOKEN is a valid mdgen access token.";
|
|
16
|
+
case 404:
|
|
17
|
+
return "Document not found (it may not exist or belong to another user).";
|
|
18
|
+
case 422:
|
|
19
|
+
return "Limit reached (documents are capped at 50 per user).";
|
|
20
|
+
case 429:
|
|
21
|
+
return `Rate limit exceeded.${retryAfter ? ` Try again in ${retryAfter}s.` : " Please wait and retry."}`;
|
|
22
|
+
default:
|
|
23
|
+
return `mdgen API error (${status})${detail ? `: ${detail}` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createMdgenClient(baseUrl, token, fetchImpl = fetch) {
|
|
27
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
28
|
+
async function request(path, init) {
|
|
29
|
+
const res = await fetchImpl(`${base}${path}`, {
|
|
30
|
+
...init,
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: `Bearer ${token}`,
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
...init?.headers,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
let detail = "";
|
|
39
|
+
try {
|
|
40
|
+
const body = (await res.json());
|
|
41
|
+
detail = body.error ?? body.code ?? "";
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// non-JSON error body
|
|
45
|
+
}
|
|
46
|
+
const retryAfter = res.status === 429 ? res.headers.get("retry-after") : null;
|
|
47
|
+
throw new MdgenApiError(res.status, humanMessage(res.status, detail, retryAfter));
|
|
48
|
+
}
|
|
49
|
+
return res;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
async listDocuments() {
|
|
53
|
+
const data = (await (await request("/api/documents")).json());
|
|
54
|
+
return data.items.map((d) => ({
|
|
55
|
+
id: d.id,
|
|
56
|
+
title: d.title,
|
|
57
|
+
mode: d.mode,
|
|
58
|
+
updatedAt: d.updatedAt,
|
|
59
|
+
}));
|
|
60
|
+
},
|
|
61
|
+
async readDocument(id) {
|
|
62
|
+
return (await request(`/api/documents/${encodeURIComponent(id)}`)).json();
|
|
63
|
+
},
|
|
64
|
+
async createDocument(input) {
|
|
65
|
+
const res = await request("/api/documents", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
markdown: input.markdown,
|
|
69
|
+
title: input.title ?? "",
|
|
70
|
+
mode: input.mode ?? "pdf",
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
return res.json();
|
|
74
|
+
},
|
|
75
|
+
// 指定フィールドのみ変更する。API の PUT は全項目を上書きするため、
|
|
76
|
+
// 既存を読んで未指定フィールドを保持してから送る(title/mode の消失を防ぐ)。
|
|
77
|
+
async updateDocument(id, patch) {
|
|
78
|
+
const current = (await (await request(`/api/documents/${encodeURIComponent(id)}`)).json());
|
|
79
|
+
await request(`/api/documents/${encodeURIComponent(id)}`, {
|
|
80
|
+
method: "PUT",
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
title: patch.title ?? current.title,
|
|
83
|
+
markdown: patch.markdown ?? current.markdown,
|
|
84
|
+
mode: patch.mode ?? current.mode,
|
|
85
|
+
settings: current.settings ?? undefined,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
async deleteDocument(id) {
|
|
90
|
+
await request(`/api/documents/${encodeURIComponent(id)}`, {
|
|
91
|
+
method: "DELETE",
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mdgen MCP サーバ(#151)。
|
|
3
|
+
// ユーザー自身の MCP クライアント(Claude Desktop / Claude Code / Codex 等)から、
|
|
4
|
+
// ユーザーの PAT(MDGEN_TOKEN)で mdgen のドキュメントを読み書きする stdio サーバ。
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { createMdgenClient, MdgenApiError } from "./client.js";
|
|
9
|
+
const DEFAULT_API_URL = "https://api.mdgen.app";
|
|
10
|
+
const apiUrl = process.env.MDGEN_API_URL ?? DEFAULT_API_URL;
|
|
11
|
+
const token = process.env.MDGEN_TOKEN;
|
|
12
|
+
if (!token) {
|
|
13
|
+
console.error("MDGEN_TOKEN is required. Create a token in mdgen (logged in) and set it as MDGEN_TOKEN.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const client = createMdgenClient(apiUrl, token);
|
|
17
|
+
/** ツールハンドラ共通: 例外を人間可読なエラー結果に変換する。 */
|
|
18
|
+
async function run(fn) {
|
|
19
|
+
try {
|
|
20
|
+
return { content: [{ type: "text", text: await fn() }] };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const message = error instanceof MdgenApiError
|
|
24
|
+
? error.message
|
|
25
|
+
: error instanceof Error
|
|
26
|
+
? error.message
|
|
27
|
+
: String(error);
|
|
28
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const server = new McpServer({ name: "mdgen-mcp", version: "0.1.0" });
|
|
32
|
+
server.registerTool("list_documents", {
|
|
33
|
+
title: "List documents",
|
|
34
|
+
description: "List the current user's saved mdgen documents (id, title, mode, updatedAt).",
|
|
35
|
+
inputSchema: {},
|
|
36
|
+
}, async () => run(async () => {
|
|
37
|
+
const items = await client.listDocuments();
|
|
38
|
+
if (items.length === 0)
|
|
39
|
+
return "No documents.";
|
|
40
|
+
return JSON.stringify(items, null, 2);
|
|
41
|
+
}));
|
|
42
|
+
server.registerTool("read_document", {
|
|
43
|
+
title: "Read document",
|
|
44
|
+
description: "Get the full Markdown and metadata of a document by id.",
|
|
45
|
+
inputSchema: { id: z.string().describe("Document id") },
|
|
46
|
+
}, async ({ id }) => run(async () => {
|
|
47
|
+
const doc = await client.readDocument(id);
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
id: doc.id,
|
|
50
|
+
title: doc.title,
|
|
51
|
+
mode: doc.mode,
|
|
52
|
+
markdown: doc.markdown,
|
|
53
|
+
}, null, 2);
|
|
54
|
+
}));
|
|
55
|
+
server.registerTool("create_document", {
|
|
56
|
+
title: "Create document",
|
|
57
|
+
description: "Create a new mdgen document. Returns the created document id.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
markdown: z.string().describe("Markdown body"),
|
|
60
|
+
title: z.string().optional().describe("Optional title"),
|
|
61
|
+
mode: z
|
|
62
|
+
.enum(["pdf", "slide"])
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Output mode (default: pdf)"),
|
|
65
|
+
},
|
|
66
|
+
}, async ({ markdown, title, mode }) => run(async () => {
|
|
67
|
+
const doc = await client.createDocument({ markdown, title, mode });
|
|
68
|
+
return `Created document ${doc.id}.`;
|
|
69
|
+
}));
|
|
70
|
+
server.registerTool("update_document", {
|
|
71
|
+
title: "Update document",
|
|
72
|
+
description: "Update a document by id. Only the provided fields change; others are preserved.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
id: z.string().describe("Document id"),
|
|
75
|
+
markdown: z.string().optional().describe("New Markdown body"),
|
|
76
|
+
title: z.string().optional().describe("New title"),
|
|
77
|
+
mode: z.enum(["pdf", "slide"]).optional().describe("New output mode"),
|
|
78
|
+
},
|
|
79
|
+
}, async ({ id, markdown, title, mode }) => run(async () => {
|
|
80
|
+
await client.updateDocument(id, { markdown, title, mode });
|
|
81
|
+
return `Updated document ${id}.`;
|
|
82
|
+
}));
|
|
83
|
+
server.registerTool("delete_document", {
|
|
84
|
+
title: "Delete document",
|
|
85
|
+
description: "Delete a document by id.",
|
|
86
|
+
inputSchema: { id: z.string().describe("Document id") },
|
|
87
|
+
}, async ({ id }) => run(async () => {
|
|
88
|
+
await client.deleteDocument(id);
|
|
89
|
+
return `Deleted document ${id}.`;
|
|
90
|
+
}));
|
|
91
|
+
const transport = new StdioServerTransport();
|
|
92
|
+
await server.connect(transport);
|
|
93
|
+
// stdio サーバはここで待機する(プロセスは MCP クライアントが管理)。
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mdgen-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for mdgen — read/write your mdgen documents from Claude / Codex and other MCP clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mdgen-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "pnpm build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"modelcontextprotocol",
|
|
26
|
+
"mdgen",
|
|
27
|
+
"markdown",
|
|
28
|
+
"claude"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/Shien-Inc/markdown-to-pdf.git",
|
|
34
|
+
"directory": "mcp"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
41
|
+
"zod": "^3.24.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22",
|
|
45
|
+
"tsx": "^4.19.0",
|
|
46
|
+
"typescript": "~5.9.3",
|
|
47
|
+
"vitest": "^4.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|