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 +9 -0
- package/.github/workflows/publish.yml +80 -0
- package/.github/workflows/registry-status.yml +47 -0
- package/Dockerfile +20 -0
- package/README.md +45 -12
- package/package.json +2 -1
- package/server.json +8 -2
- package/src/http.js +75 -0
- package/src/index.js +7 -938
- package/src/server.js +1021 -0
package/.dockerignore
ADDED
|
@@ -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
|
-
|
|
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
|
-
##
|
|
11
|
+
## Hosted or local
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
##
|
|
108
|
+
## Client configuration
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
### Hosted
|
|
111
|
+
|
|
112
|
+
Claude Code:
|
|
98
113
|
|
|
99
114
|
```bash
|
|
100
|
-
|
|
115
|
+
claude mcp add --transport http postmd https://postmd.turink.com/mcp
|
|
101
116
|
```
|
|
102
117
|
|
|
103
|
-
|
|
104
|
-
will sit there waiting for a client.
|
|
118
|
+
Clients that take a JSON config:
|
|
105
119
|
|
|
106
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
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
|
+
});
|