postmd-mcp-server 2.3.0 → 2.5.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 +107 -0
- package/.github/workflows/registry-status.yml +47 -0
- package/Dockerfile +20 -0
- package/README.md +64 -22
- package/package.json +5 -3
- package/scripts/smoke-test.mjs +6 -17
- package/server.json +10 -4
- package/src/http.js +75 -0
- package/src/index.js +7 -938
- package/src/server.js +944 -0
package/.dockerignore
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
# 레지스트리는 소유권을 npm 으로 확인한다. 방금 올린 판의 메타데이터를 받아 mcpName 이
|
|
78
|
+
# 서버 이름과 같은지 본다. 그런데 npm 에 올린 직후에는 그 버전이 아직 보이지 않아
|
|
79
|
+
# 곧바로 부르면 404 로 떨어진다. 2.4.0 이 실제로 이렇게 실패했다 - npm 에는 올라갔는데
|
|
80
|
+
# 레지스트리만 빠져 두 곳이 어긋났다. 보일 때까지 기다린 다음 넘어간다.
|
|
81
|
+
- name: Wait until npm serves this version
|
|
82
|
+
run: |
|
|
83
|
+
NAME=$(node -p "require('./package.json').name")
|
|
84
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
85
|
+
for i in $(seq 1 30); do
|
|
86
|
+
MCP_NAME=$(curl -fsS "https://registry.npmjs.org/$NAME/$VERSION" 2>/dev/null \
|
|
87
|
+
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{process.stdout.write(JSON.parse(s).mcpName||'')}catch(e){}})")
|
|
88
|
+
if [ -n "$MCP_NAME" ]; then
|
|
89
|
+
echo "npm serves $NAME@$VERSION with mcpName $MCP_NAME"
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
echo "attempt $i/30: not visible yet"
|
|
93
|
+
sleep 10
|
|
94
|
+
done
|
|
95
|
+
echo "::error::npm did not serve $NAME@$VERSION within 5 minutes"
|
|
96
|
+
exit 1
|
|
97
|
+
|
|
98
|
+
# 기다린 뒤에도 순간적으로 실패할 수 있다. 같은 버전을 다시 올리는 것은 문제가 없다.
|
|
99
|
+
- name: Publish to the MCP registry
|
|
100
|
+
run: |
|
|
101
|
+
./mcp-publisher login github-oidc
|
|
102
|
+
for i in 1 2 3; do
|
|
103
|
+
if ./mcp-publisher publish; then exit 0; fi
|
|
104
|
+
echo "publish failed ($i/3), retrying in 20s"
|
|
105
|
+
sleep 20
|
|
106
|
+
done
|
|
107
|
+
exit 1
|
|
@@ -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,29 @@
|
|
|
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, and add a document graph that the viewer draws beside the text: the reading order, the bodies and rules a document names, a procedure spread over its chapters. Optional groups, document passwords 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:
|
|
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: organizing documents in groups, and notes.
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**30-day retention.** Documents have a 30-day retention period (`data.retainedUntil`) that extends by 30 days whenever the document is read (at most once per day). Documents without a password can be updated or deleted by anyone; password-protected documents require the password or the owner's API key.
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**Where things are written down:** [/llms.txt](https://postmd.turink.com/llms.txt) lists what the service can do and which page answers each thing; it is the place to start. [/docs/api](https://postmd.turink.com/docs/api) is the HTTP reference and [/api-docs](https://postmd.turink.com/api-docs) the machine-readable spec.
|
|
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 20 |
|
|
18
|
+
| API key | not accepted | optional, for member-scoped 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. Groups, notes and the tools that read a file from your disk are local-only.
|
|
15
23
|
|
|
16
24
|
## Configuration
|
|
17
25
|
|
|
18
|
-
All
|
|
26
|
+
Local only — the hosted server reads none of these. All are optional.
|
|
19
27
|
|
|
20
28
|
| Variable | Description |
|
|
21
29
|
|----------|-------------|
|
|
@@ -27,6 +35,9 @@ Load order: this repo's `.env` (if present) is applied via `dotenv` without over
|
|
|
27
35
|
|
|
28
36
|
## Tools
|
|
29
37
|
|
|
38
|
+
Every tool below works on the local server. The hosted server carries five of them:
|
|
39
|
+
`postmd_create_document`, `postmd_get_document`, `postmd_get_document_raw`, `postmd_update_document`, and `postmd_delete_document`.
|
|
40
|
+
|
|
30
41
|
Publishing and reading — no key needed:
|
|
31
42
|
|
|
32
43
|
| Tool | Purpose |
|
|
@@ -36,18 +47,29 @@ Publishing and reading — no key needed:
|
|
|
36
47
|
| `postmd_get_document` | Metadata by `docCode` |
|
|
37
48
|
| `postmd_get_document_raw` | Stored Markdown body (optional `password`) |
|
|
38
49
|
|
|
39
|
-
Managing documents — key with `documents:write
|
|
40
|
-
|
|
41
|
-
Each of the first three also accepts `controlToken` instead of a key, for a document published anonymously.
|
|
50
|
+
Managing documents — key with `documents:write` (or password / no credential for unowned documents without password):
|
|
42
51
|
|
|
43
52
|
| Tool | Purpose |
|
|
44
53
|
|------|---------|
|
|
45
|
-
| `postmd_update_document` | Replace content and/or metadata; can clear password
|
|
54
|
+
| `postmd_update_document` | Replace content and/or metadata; can clear password |
|
|
46
55
|
| `postmd_update_document_from_file` | Same, body read from a local `filePath` |
|
|
47
56
|
| `postmd_delete_document` | Delete a document (no undo) |
|
|
48
|
-
| `postmd_upload_attachment` | Upload an image/PDF, get a URL to embed in Markdown |
|
|
49
57
|
| `postmd_create_documents_from_files` | Bulk-publish several `.md` files in one call |
|
|
50
|
-
| `postmd_move_document_to_group` | Move a document into a group
|
|
58
|
+
| `postmd_move_document_to_group` | Move a document into a group |
|
|
59
|
+
|
|
60
|
+
### Graphs
|
|
61
|
+
|
|
62
|
+
A PostMD document can carry graph data that the viewer draws in a panel beside the text, showing
|
|
63
|
+
how the parts of the document relate. No tool here creates or edits a graph, because there is no
|
|
64
|
+
endpoint for one: the data sits in the Markdown as an HTML comment and travels with the body.
|
|
65
|
+
|
|
66
|
+
Adding a graph to an existing document therefore means reading it with `postmd_get_document_raw`,
|
|
67
|
+
inserting the comment, and sending the whole body back with `postmd_update_document`. Publishing a
|
|
68
|
+
new document with a graph is an ordinary `postmd_create_document` call.
|
|
69
|
+
|
|
70
|
+
The format is at <https://postmd.turink.com/docs/graph>. Working out what the nodes are and how
|
|
71
|
+
they connect requires reading the document, which is the calling agent's part; PostMD only draws
|
|
72
|
+
what it finds.
|
|
51
73
|
|
|
52
74
|
### Replacing content on a document that has notes
|
|
53
75
|
|
|
@@ -87,23 +109,40 @@ Groups — key with `groups:read` / `groups:write`:
|
|
|
87
109
|
| `postmd_list_groups` | Groups visible to the key (paged) |
|
|
88
110
|
| `postmd_list_group_documents` | Documents in a group (paged, searchable, sortable) |
|
|
89
111
|
| `postmd_create_group` | New group |
|
|
90
|
-
| `postmd_update_group` | Rename
|
|
112
|
+
| `postmd_update_group` | Rename group |
|
|
91
113
|
| `postmd_delete_group` | Delete a group (documents survive) |
|
|
92
114
|
|
|
93
115
|
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
116
|
|
|
95
|
-
##
|
|
117
|
+
## Client configuration
|
|
118
|
+
|
|
119
|
+
### Hosted
|
|
96
120
|
|
|
97
|
-
|
|
121
|
+
Claude Code:
|
|
98
122
|
|
|
99
123
|
```bash
|
|
100
|
-
|
|
124
|
+
claude mcp add --transport http postmd https://postmd.turink.com/mcp
|
|
101
125
|
```
|
|
102
126
|
|
|
103
|
-
|
|
104
|
-
will sit there waiting for a client.
|
|
127
|
+
Clients that take a JSON config:
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"PostMD": {
|
|
133
|
+
"type": "http",
|
|
134
|
+
"url": "https://postmd.turink.com/mcp"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In ChatGPT, add it under **Settings → Apps**; in claude.ai, under **Settings → Connectors**.
|
|
141
|
+
There is nothing to authorize.
|
|
142
|
+
|
|
143
|
+
### Local
|
|
144
|
+
|
|
145
|
+
There is nothing to install: `npx` fetches the package and the client spawns it.
|
|
107
146
|
|
|
108
147
|
Claude Code:
|
|
109
148
|
|
|
@@ -137,6 +176,9 @@ claude mcp add postmd-dev -- node "$PWD/src/index.js"
|
|
|
137
176
|
|
|
138
177
|
Leave `env` out entirely for publish/read-only use. `cp .env.example .env` works too — the server loads its own `.env`.
|
|
139
178
|
|
|
179
|
+
Running `npx -y postmd-mcp-server` by hand only checks that it starts. It speaks MCP over
|
|
180
|
+
stdin and stdout, so it will sit there waiting for a client.
|
|
181
|
+
|
|
140
182
|
## Smoke test
|
|
141
183
|
|
|
142
184
|
Runs the full write path against a live server and cleans up after itself. Needs a key with all four scopes.
|
|
@@ -146,7 +188,7 @@ export POSTMD_API_KEY=pmk_…
|
|
|
146
188
|
npm run smoke
|
|
147
189
|
```
|
|
148
190
|
|
|
149
|
-
Creates a group and a passworded document, reads it back, updates it, clears the password, then deletes both. It also publishes one document with no credential and removes it
|
|
191
|
+
Creates a group and a passworded document, reads it back, updates it, clears the password, then deletes both. It also publishes one document with no credential and removes it without a token.
|
|
150
192
|
|
|
151
193
|
## Stack
|
|
152
194
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postmd-mcp-server",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "MCP server for PostMD — publish Markdown
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "MCP server for PostMD — publish Markdown as a shareable web page, with document graphs beside the text",
|
|
5
5
|
"mcpName": "io.github.reinlainer/postmd-mcp-server",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.js",
|
|
@@ -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": {
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
"mcp",
|
|
25
26
|
"postmd",
|
|
26
27
|
"markdown",
|
|
27
|
-
"publishing"
|
|
28
|
+
"publishing",
|
|
29
|
+
"document-graph"
|
|
28
30
|
],
|
|
29
31
|
"license": "MIT",
|
|
30
32
|
"dependencies": {
|
package/scripts/smoke-test.mjs
CHANGED
|
@@ -125,10 +125,9 @@ if (groupId != null) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/*
|
|
128
|
-
9. 익명
|
|
128
|
+
9. 익명 발행 (비밀번호 없는 문서).
|
|
129
129
|
|
|
130
|
-
자격 증명을 붙이지 않고 부른다.
|
|
131
|
-
여기서만 Authorization 헤더를 뺀다.
|
|
130
|
+
자격 증명을 붙이지 않고 부른다.
|
|
132
131
|
*/
|
|
133
132
|
const anonForm = new FormData();
|
|
134
133
|
anonForm.append(
|
|
@@ -141,32 +140,22 @@ const anon = await anonRes.json().catch(() => null);
|
|
|
141
140
|
check("publish without a credential", anon?.resultCode === "200", JSON.stringify(anon));
|
|
142
141
|
|
|
143
142
|
const anonCode = anon?.data?.docCode;
|
|
144
|
-
|
|
145
|
-
check("answer carries a control token", typeof controlToken === "string" && controlToken.startsWith("pmt_"));
|
|
146
|
-
check("answer carries a deletion date", typeof anon?.data?.retainedUntil === "string");
|
|
143
|
+
check("answer carries a retention date", typeof anon?.data?.retainedUntil === "string");
|
|
147
144
|
check("answer explains the terms", typeof anon?.message === "string" && anon.message.length > 0);
|
|
148
145
|
|
|
149
|
-
if (anonCode
|
|
146
|
+
if (anonCode) {
|
|
150
147
|
const titled = new FormData();
|
|
151
148
|
titled.append("title", `mcp smoke anon ${stamp}`);
|
|
152
149
|
const changed = await fetch(`${base}/api/v1/documents/${anonCode}/update`, {
|
|
153
150
|
method: "POST",
|
|
154
|
-
headers: { "X-Document-Token": controlToken },
|
|
155
151
|
body: titled,
|
|
156
152
|
});
|
|
157
|
-
check("
|
|
158
|
-
|
|
159
|
-
const refused = await fetch(`${base}/api/v1/documents/${anonCode}/delete`, {
|
|
160
|
-
method: "POST",
|
|
161
|
-
headers: { "X-Document-Token": "pmt_wrong" },
|
|
162
|
-
});
|
|
163
|
-
check("a wrong token is refused", (await refused.json().catch(() => null))?.resultCode === "E_DOC_0008");
|
|
153
|
+
check("anyone can update a document without password", (await changed.json().catch(() => null))?.resultCode === "200");
|
|
164
154
|
|
|
165
155
|
const removed = await fetch(`${base}/api/v1/documents/${anonCode}/delete`, {
|
|
166
156
|
method: "POST",
|
|
167
|
-
headers: { "X-Document-Token": controlToken },
|
|
168
157
|
});
|
|
169
|
-
check("
|
|
158
|
+
check("anyone can delete a document without password", (await removed.json().catch(() => null))?.resultCode === "200");
|
|
170
159
|
}
|
|
171
160
|
|
|
172
161
|
console.log(failures ? `\n${failures} failure(s)` : "\nall good");
|
package/server.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.reinlainer/postmd-mcp-server",
|
|
4
|
-
"description": "Publish Markdown
|
|
5
|
-
"version": "2.
|
|
4
|
+
"description": "Publish Markdown as a shareable web page, with document graphs drawn beside the text",
|
|
5
|
+
"version": "2.5.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.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "POSTMD_API_KEY",
|
|
27
|
-
"description": "Needed only for managing documents
|
|
27
|
+
"description": "Needed only for managing documents and groups. Publishing and reading work without it.",
|
|
28
28
|
"isRequired": false,
|
|
29
29
|
"isSecret": true
|
|
30
30
|
},
|
|
@@ -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
|
+
});
|