postmd-mcp-server 2.3.0 → 2.4.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/.dockerignore ADDED
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ .git
3
+ .github
4
+ .env
5
+ .env.*
6
+ *.log
7
+ scripts
8
+ README.md
9
+ LICENSE
@@ -0,0 +1,80 @@
1
+ # 태그를 밀면 npm 과 MCP 레지스트리에 올린다.
2
+ #
3
+ # 두 곳 모두 토큰을 두지 않는다. GitHub 이 발급하는 단기 OIDC 토큰으로 인증한다.
4
+ #
5
+ # npm 트러스티드 퍼블리셔. npmjs.com 의 패키지 설정에 이 저장소와 이 파일 이름을
6
+ # 한 번 등록해 두면 된다. 등록한 워크플로에서 온 요청만 받는다.
7
+ # MCP 레지스트리 io.github.<소유자> 네임스페이스는 그 소유자의 저장소에서 온 OIDC 토큰으로
8
+ # 증명된다. 별도 설정이 없다.
9
+ #
10
+ # 사람이 브라우저에서 승인하는 단계는 없다. 손으로 올리던 때는 npm 은 보안 키로 2차 인증을,
11
+ # 레지스트리는 GitHub 기기 흐름 승인을 매번 요구했다.
12
+ name: publish
13
+
14
+ on:
15
+ push:
16
+ tags: ['v*']
17
+
18
+ jobs:
19
+ publish:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ # 두 곳의 인증이 모두 이 토큰으로 이루어진다.
23
+ id-token: write
24
+ contents: read
25
+
26
+ steps:
27
+ - uses: actions/checkout@v5
28
+
29
+ - uses: actions/setup-node@v5
30
+ with:
31
+ node-version: '22'
32
+ registry-url: 'https://registry.npmjs.org'
33
+
34
+ # 트러스티드 퍼블리싱은 npm 11.5.1 이상에서만 된다. 러너에 실린 판을 믿지 않는다.
35
+ - name: Use a recent npm
36
+ run: npm install -g npm@latest
37
+
38
+ # 세 곳의 버전이 어긋나면 레지스트리가 거절한다. 올리기 전에 여기서 잡는다.
39
+ - name: Versions must match the tag
40
+ run: |
41
+ TAG="${GITHUB_REF#refs/tags/v}"
42
+ PKG=$(node -p "require('./package.json').version")
43
+ SRV=$(node -p "require('./server.json').version")
44
+ SRVPKG=$(node -p "require('./server.json').packages[0].version")
45
+ echo "tag=$TAG package.json=$PKG server.json=$SRV server.json/packages=$SRVPKG"
46
+ for v in "$PKG" "$SRV" "$SRVPKG"; do
47
+ if [ "$v" != "$TAG" ]; then
48
+ echo "::error::version mismatch — tag is $TAG"
49
+ exit 1
50
+ fi
51
+ done
52
+
53
+ - run: npm ci
54
+
55
+ - name: Syntax check
56
+ run: |
57
+ node --check src/index.js
58
+ node --check src/env.js
59
+ node --check scripts/smoke-test.mjs
60
+
61
+ # 이미 올라간 버전을 다시 올리면 npm 이 403 을 준다. 태그를 다시 밀거나 레지스트리
62
+ # 쪽만 다시 올려야 할 때 이 단계에서 멈추지 않도록 한다.
63
+ - name: Publish to npm
64
+ run: |
65
+ VERSION=$(node -p "require('./package.json').version")
66
+ NAME=$(node -p "require('./package.json').name")
67
+ if npm view "$NAME@$VERSION" version > /dev/null 2>&1; then
68
+ echo "$NAME@$VERSION is already on npm — skipping"
69
+ else
70
+ npm publish --access public
71
+ fi
72
+
73
+ - name: Install mcp-publisher
74
+ run: |
75
+ curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
76
+
77
+ - name: Publish to the MCP registry
78
+ run: |
79
+ ./mcp-publisher login github-oidc
80
+ ./mcp-publisher publish
@@ -0,0 +1,47 @@
1
+ # 레지스트리에 올라간 버전의 상태를 바꾼다. 손으로 돌린다.
2
+ #
3
+ # 잘못 올린 버전을 정리하는 데 쓴다. 레지스트리는 semver 가 가장 높은 것을 최신으로 보므로,
4
+ # 실수로 올린 높은 번호를 그냥 두면 그것이 계속 최신으로 남는다.
5
+ #
6
+ # 인증은 발행과 같다. GitHub 이 발급하는 단기 OIDC 토큰이라 사람이 승인할 것이 없다.
7
+ name: registry-status
8
+
9
+ on:
10
+ workflow_dispatch:
11
+ inputs:
12
+ version:
13
+ description: '상태를 바꿀 버전 (예: 3.0.0)'
14
+ required: true
15
+ status:
16
+ description: '바꿀 상태'
17
+ required: true
18
+ default: deleted
19
+ type: choice
20
+ options: [active, deprecated, deleted]
21
+ message:
22
+ description: '이유. 레지스트리에 함께 남는다'
23
+ required: false
24
+ default: ''
25
+
26
+ jobs:
27
+ status:
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ id-token: write
31
+ contents: read
32
+
33
+ steps:
34
+ - uses: actions/checkout@v5
35
+
36
+ - name: Install mcp-publisher
37
+ run: |
38
+ curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
39
+
40
+ - name: Set the version status
41
+ run: |
42
+ NAME=$(node -p "require('./server.json').name")
43
+ ./mcp-publisher login github-oidc
44
+ ./mcp-publisher status \
45
+ --status "${{ inputs.status }}" \
46
+ --message "${{ inputs.message }}" \
47
+ "$NAME" "${{ inputs.version }}"
package/Dockerfile ADDED
@@ -0,0 +1,20 @@
1
+ # 원격(streamable HTTP) 서버 이미지. stdio 로 쓰는 사람은 이 이미지가 필요 없다 —
2
+ # npx 로 바로 돌아간다.
3
+ FROM node:22-alpine
4
+
5
+ WORKDIR /app
6
+
7
+ # 의존성 먼저. 소스만 바뀔 때 이 층을 다시 받지 않는다.
8
+ COPY package.json package-lock.json ./
9
+ RUN npm ci --omit=dev
10
+
11
+ COPY src ./src
12
+
13
+ # 루트로 돌릴 이유가 없다. node 사용자는 베이스 이미지에 이미 있다.
14
+ USER node
15
+
16
+ ENV NODE_ENV=production
17
+ ENV PORT=8080
18
+ EXPOSE 8080
19
+
20
+ CMD ["node", "src/http.js"]
package/README.md CHANGED
@@ -1,21 +1,30 @@
1
1
  # PostMD MCP Server
2
2
 
3
- stdio [Model Context Protocol](https://modelcontextprotocol.io) server for **[PostMD](https://postmd.turink.com)** — publish a Markdown document, get a web page you share by link. Optional groups, document passwords, share expiry and viewer themes. This server wraps PostMD's public API (`/api/v1`) so assistants can publish, read, update and organize documents.
3
+ [Model Context Protocol](https://modelcontextprotocol.io) server for **[PostMD](https://postmd.turink.com)** — publish a Markdown document, get a web page you share by link. Optional groups, document passwords, share expiry and viewer themes. This server wraps PostMD's public API (`/api/v1`) so assistants can publish, read, update and organize documents.
4
4
 
5
- **Publishing needs no account and no key.** With zero configuration this server can already turn Markdown into a shareable page. An API key adds management: updating and deleting your documents, attachments, and groups.
5
+ **Publishing needs no account and no key.** With zero configuration this server can already turn Markdown into a shareable page, and the hosted server at `https://postmd.turink.com/mcp` needs no install either. An API key adds management: updating and deleting your documents, attachments, and groups.
6
6
 
7
7
  **Anonymous documents come with a control token.** Publishing without a key returns `data.controlToken` and `data.retainedUntil`: the document is deleted at that instant, and the token is the only way to update or delete it before then. It is shown once and cannot be reissued, so keep it with the `docCode`. Pass it as `controlToken` to the update and delete tools and they work without an API key.
8
8
 
9
9
  **HTTP reference:** [postmd.turink.com/docs/api](https://postmd.turink.com/docs/api) · machine-readable spec at [/api-docs](https://postmd.turink.com/api-docs)
10
10
 
11
- ## Requirements
11
+ ## Hosted or local
12
12
 
13
- - **Node.js** 20 or later
14
- - Nothing else. An **API key** (`pmk_…`) only for the management tools.
13
+ | | Hosted | Local |
14
+ |---|---|---|
15
+ | Address | `https://postmd.turink.com/mcp` | `npx -y postmd-mcp-server` |
16
+ | Needs | nothing | Node.js 20 or later |
17
+ | Tools | 5 | all 21 |
18
+ | API key | not accepted | optional, for the management tools |
19
+ | Clients | any, including web-only ones such as ChatGPT and claude.ai | any that can run a local process |
20
+
21
+ The hosted server has no way to receive an API key, so it carries only the tools that need
22
+ none. Attachments, groups, notes and the tools that read a file from your disk are
23
+ local-only.
15
24
 
16
25
  ## Configuration
17
26
 
18
- All variables are optional.
27
+ Local only — the hosted server reads none of these. All are optional.
19
28
 
20
29
  | Variable | Description |
21
30
  |----------|-------------|
@@ -27,6 +36,10 @@ Load order: this repo's `.env` (if present) is applied via `dotenv` without over
27
36
 
28
37
  ## Tools
29
38
 
39
+ Every tool below works on the local server. The hosted server carries five of them:
40
+ `postmd_create_document`, `postmd_get_document`, `postmd_get_document_raw`, and — with a
41
+ `controlToken` instead of a key — `postmd_update_document` and `postmd_delete_document`.
42
+
30
43
  Publishing and reading — no key needed:
31
44
 
32
45
  | Tool | Purpose |
@@ -92,18 +105,35 @@ Groups — key with `groups:read` / `groups:write`:
92
105
 
93
106
  For uploads: either pass the full Markdown as the `markdown` argument, or pass a local `filePath` only so this server reads the file. The path must exist on the machine running the MCP server.
94
107
 
95
- ## Quickstart
108
+ ## Client configuration
96
109
 
97
- Nothing to install. `npx` fetches the package and the MCP client spawns it.
110
+ ### Hosted
111
+
112
+ Claude Code:
98
113
 
99
114
  ```bash
100
- npx -y postmd-mcp-server
115
+ claude mcp add --transport http postmd https://postmd.turink.com/mcp
101
116
  ```
102
117
 
103
- Run it by hand only to check that it starts — it speaks MCP over stdin and stdout, so it
104
- will sit there waiting for a client.
118
+ Clients that take a JSON config:
105
119
 
106
- ## Client configuration
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "PostMD": {
124
+ "type": "http",
125
+ "url": "https://postmd.turink.com/mcp"
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ In ChatGPT, add it under **Settings → Apps**; in claude.ai, under **Settings → Connectors**.
132
+ There is nothing to authorize.
133
+
134
+ ### Local
135
+
136
+ There is nothing to install: `npx` fetches the package and the client spawns it.
107
137
 
108
138
  Claude Code:
109
139
 
@@ -137,6 +167,9 @@ claude mcp add postmd-dev -- node "$PWD/src/index.js"
137
167
 
138
168
  Leave `env` out entirely for publish/read-only use. `cp .env.example .env` works too — the server loads its own `.env`.
139
169
 
170
+ Running `npx -y postmd-mcp-server` by hand only checks that it starts. It speaks MCP over
171
+ stdin and stdout, so it will sit there waiting for a client.
172
+
140
173
  ## Smoke test
141
174
 
142
175
  Runs the full write path against a live server and cleans up after itself. Needs a key with all four scopes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postmd-mcp-server",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "MCP server for PostMD — publish Markdown, get a shareable web page",
5
5
  "mcpName": "io.github.reinlainer/postmd-mcp-server",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "start": "node src/index.js",
13
+ "start:http": "node src/http.js",
13
14
  "smoke": "node scripts/smoke-test.mjs"
14
15
  },
15
16
  "engines": {
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.reinlainer/postmd-mcp-server",
4
4
  "description": "Publish Markdown to PostMD and get a shareable web page",
5
- "version": "2.3.0",
5
+ "version": "2.4.0",
6
6
  "repository": {
7
7
  "url": "https://github.com/reinlainer/postmd-mcp-server",
8
8
  "source": "github"
@@ -11,7 +11,7 @@
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "postmd-mcp-server",
14
- "version": "2.3.0",
14
+ "version": "2.4.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -36,5 +36,11 @@
36
36
  }
37
37
  ]
38
38
  }
39
+ ],
40
+ "remotes": [
41
+ {
42
+ "type": "streamable-http",
43
+ "url": "https://postmd.turink.com/mcp"
44
+ }
39
45
  ]
40
46
  }
package/src/http.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 원격 진입점. streamable HTTP 로 받는다.
4
+ *
5
+ * 웹에서 도는 클라이언트(ChatGPT, claude.ai)는 로컬 프로세스를 띄우지 못해 stdio 서버에
6
+ * 붙을 수 없다. 그쪽이 요구하는 것은 고정 HTTPS 주소 하나다.
7
+ *
8
+ * 도구는 `server.js` 가 갖고 있고 여기서 여는 것은 그중 자격 증명 없이 되는 다섯 개다
9
+ * (`REMOTE_TOOLS`). 그래서 이 서버에는 인증이 없다.
10
+ *
11
+ * 상태를 두지 않는다(stateless). 요청마다 서버와 전송을 새로 만들고 끝나면 버린다.
12
+ * 도구가 모두 API 한 번 부르고 끝나는 것이라 요청 사이에 이어 둘 것이 없고, 세션을 들면
13
+ * 그때부터 메모리에 남는 것이 생겨 컨테이너를 늘릴 때 붙는 자리가 된다.
14
+ */
15
+ import http from "node:http";
16
+ import process from "node:process";
17
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
+ import { createMcpServer, VERSION } from "./server.js";
19
+
20
+ const PORT = Number(process.env.PORT || process.env.POSTMD_MCP_PORT || 8080);
21
+ const MCP_PATH = "/mcp";
22
+
23
+ /**
24
+ * 본문 상한. 문서 본문이 JSON 안에 실려 오므로 업로드 상한보다 넉넉해야 하지만, 열어
25
+ * 두면 아무나 부르는 자리에서 메모리를 밀어 넣을 수 있다. 실제 문서 크기 판정은 API 가
26
+ * 한다 - 여기는 그 앞의 거친 그물이다.
27
+ */
28
+ const MAX_BODY = 8 * 1024 * 1024;
29
+
30
+ function json(res, status, body) {
31
+ const payload = JSON.stringify(body);
32
+ res.writeHead(status, { "content-type": "application/json" });
33
+ res.end(payload);
34
+ }
35
+
36
+ const httpServer = http.createServer(async (req, res) => {
37
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
38
+
39
+ // 컨테이너와 프록시가 살아 있는지 보는 자리. MCP 와 무관하다.
40
+ if (url.pathname === "/health") {
41
+ return json(res, 200, { status: "ok", version: VERSION });
42
+ }
43
+
44
+ if (url.pathname !== MCP_PATH) {
45
+ return json(res, 404, {
46
+ error: `Not found. This server speaks MCP over streamable HTTP at ${MCP_PATH}.`,
47
+ });
48
+ }
49
+
50
+ const length = Number(req.headers["content-length"] || 0);
51
+ if (length > MAX_BODY) {
52
+ return json(res, 413, { error: "Request body too large." });
53
+ }
54
+
55
+ // 요청 하나에 서버 하나. 끝나면 둘 다 닫아 아무것도 남기지 않는다.
56
+ const server = createMcpServer({ remote: true });
57
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
58
+
59
+ res.on("close", () => {
60
+ transport.close().catch(() => {});
61
+ server.close().catch(() => {});
62
+ });
63
+
64
+ try {
65
+ await server.connect(transport);
66
+ await transport.handleRequest(req, res);
67
+ } catch (e) {
68
+ process.stderr.write(`mcp request failed: ${e instanceof Error ? e.stack : e}\n`);
69
+ if (!res.headersSent) json(res, 500, { error: "Internal error." });
70
+ }
71
+ });
72
+
73
+ httpServer.listen(PORT, () => {
74
+ process.stdout.write(`postmd-mcp-server ${VERSION} — streamable HTTP on :${PORT}${MCP_PATH}\n`);
75
+ });