block-style-cms-mcp 0.0.3__tar.gz
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.
- block_style_cms_mcp-0.0.3/.gitignore +15 -0
- block_style_cms_mcp-0.0.3/PKG-INFO +126 -0
- block_style_cms_mcp-0.0.3/README.md +102 -0
- block_style_cms_mcp-0.0.3/pyproject.toml +61 -0
- block_style_cms_mcp-0.0.3/src/block_style_cms_mcp/__init__.py +7 -0
- block_style_cms_mcp-0.0.3/src/block_style_cms_mcp/__main__.py +3 -0
- block_style_cms_mcp-0.0.3/src/block_style_cms_mcp/config.py +52 -0
- block_style_cms_mcp-0.0.3/src/block_style_cms_mcp/server.py +287 -0
- block_style_cms_mcp-0.0.3/src/block_style_cms_mcp/tools.py +34 -0
- block_style_cms_mcp-0.0.3/tests/test_config.py +62 -0
- block_style_cms_mcp-0.0.3/tests/test_server.py +101 -0
- block_style_cms_mcp-0.0.3/uv.lock +1294 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.env
|
|
3
|
+
tmp/
|
|
4
|
+
dist/
|
|
5
|
+
.venv/
|
|
6
|
+
**/__pycache__/
|
|
7
|
+
**/*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
*.pyc
|
|
11
|
+
.DS_Store
|
|
12
|
+
coverage/
|
|
13
|
+
packages/python/tests/_closed_loop_work/
|
|
14
|
+
packages/python/tests/closed_loop_report.json
|
|
15
|
+
packages/python/tests/trilingual_longform_report.json
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: block-style-cms-mcp
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: MCP server for Block Style CMS (wraps block-style-cms-sdk)
|
|
5
|
+
Author-email: Biscuit Zhou <biscuit_zhou@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: blocknote,cms,markdown,mcp
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: block-style-cms-sdk>=0.0.3
|
|
19
|
+
Requires-Dist: mcp>=2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# block-style-cms-mcp
|
|
26
|
+
|
|
27
|
+
Python [MCP](https://modelcontextprotocol.io/) server for [Block Style CMS](https://github.com/Byte-Biscuit/block-style-cms). Thin stdio wrapper around [`block-style-cms-sdk`](../python).
|
|
28
|
+
|
|
29
|
+
## Credentials (where they live)
|
|
30
|
+
|
|
31
|
+
CMS **base URL** and **API token** are **not** tool arguments. Put them in the MCP host config `env` block (e.g. Cursor `mcp.json` / Settings → MCP):
|
|
32
|
+
|
|
33
|
+
| Parameter | Env var | Required |
|
|
34
|
+
|-----------|---------|----------|
|
|
35
|
+
| CMS base URL | `BSC_BASE_URL` | yes |
|
|
36
|
+
| API token | `BSC_API_KEY` | yes |
|
|
37
|
+
| Auth style `api-key` or `bearer` | `BSC_AUTH_STYLE` | no (default `api-key`) |
|
|
38
|
+
|
|
39
|
+
Do not commit real tokens. Do not pass them as tool inputs (they would land in chat logs).
|
|
40
|
+
|
|
41
|
+
### Cursor example
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"block-style-cms": {
|
|
47
|
+
"command": "uvx",
|
|
48
|
+
"args": ["block-style-cms-mcp"],
|
|
49
|
+
"env": {
|
|
50
|
+
"BSC_BASE_URL": "https://cms.example.com",
|
|
51
|
+
"BSC_API_KEY": "your-api-token"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Local checkout (before PyPI):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"block-style-cms": {
|
|
64
|
+
"command": "uv",
|
|
65
|
+
"args": [
|
|
66
|
+
"run",
|
|
67
|
+
"--directory",
|
|
68
|
+
"/absolute/path/to/packages/mcp-python",
|
|
69
|
+
"block-style-cms-mcp"
|
|
70
|
+
],
|
|
71
|
+
"env": {
|
|
72
|
+
"BSC_BASE_URL": "https://cms.example.com",
|
|
73
|
+
"BSC_API_KEY": "your-api-token"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Tools
|
|
81
|
+
|
|
82
|
+
| Tool | Purpose |
|
|
83
|
+
|------|---------|
|
|
84
|
+
| `list_locales` | Supported article languages |
|
|
85
|
+
| `list_channels` | Site channels |
|
|
86
|
+
| `upload_image` | Upload local image |
|
|
87
|
+
| `delete_image` | Delete library image by filename |
|
|
88
|
+
| `publish_from_markdown` | Create article from markdown |
|
|
89
|
+
| `update_from_markdown` | Update article from markdown (`id` required) |
|
|
90
|
+
| `get_article` | Get by slug + id (optional `include_md_content`) |
|
|
91
|
+
| `get_article_by_slug` | Get by slug + locale (optional `include_md_content`) |
|
|
92
|
+
| `list_articles` | List metadata with filters (`published` query: `published` \| `draft` \| `all`, **not** `true`/`false`) |
|
|
93
|
+
| `update_article_metadata` | Metadata-only update |
|
|
94
|
+
| `set_article_published` | Publish / unpublish |
|
|
95
|
+
| `delete_article` | Delete by slug + id |
|
|
96
|
+
| `delete_article_by_slug` | Delete by slug + locale |
|
|
97
|
+
|
|
98
|
+
Article closed-loop design: see repo [`docs/`](../../docs/README.md).
|
|
99
|
+
|
|
100
|
+
Path arguments (`md_file`, `assets_dir`, `file_path`, `cover_file`) must be readable by the MCP process (typically paths under the host workspace).
|
|
101
|
+
|
|
102
|
+
`list_articles.published` accepts only `published` | `draft` | `all` (CMS query enum). Boolean strings fail. Create/update and `set_article_published` still use a real boolean for the article’s published flag.
|
|
103
|
+
|
|
104
|
+
## Develop
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd packages/mcp-python
|
|
108
|
+
uv sync --extra dev
|
|
109
|
+
uv run ruff check .
|
|
110
|
+
uv run pytest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Release
|
|
114
|
+
|
|
115
|
+
1. Ensure the required `block-style-cms-sdk` version is already on PyPI.
|
|
116
|
+
2. Bump `version` in `pyproject.toml` (and keep the SDK dependency pin in sync).
|
|
117
|
+
3. Tag `mcp-py-vX.Y.Z`, push, and **Publish** a GitHub Release for that tag.
|
|
118
|
+
4. GitHub Actions uploads to PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC; GitHub Environment **`pypi-mcp`**, separate from the SDK’s `pypi` environment).
|
|
119
|
+
|
|
120
|
+
Example: version `0.0.3` → tag `mcp-py-v0.0.3` → GitHub Release → PyPI.
|
|
121
|
+
|
|
122
|
+
Setup checklist:
|
|
123
|
+
|
|
124
|
+
1. GitHub → Settings → Environments → create **`pypi-mcp`**.
|
|
125
|
+
2. Deployment branches and tags: allow tag pattern **`mcp-py-v*`** (optionally branch `master` for manual dry-runs).
|
|
126
|
+
3. On PyPI, register a Trusted Publisher for project `block-style-cms-mcp` pointing at workflow `mcp-python-release.yml` (and environment name `pypi-mcp` if the form asks for it).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# block-style-cms-mcp
|
|
2
|
+
|
|
3
|
+
Python [MCP](https://modelcontextprotocol.io/) server for [Block Style CMS](https://github.com/Byte-Biscuit/block-style-cms). Thin stdio wrapper around [`block-style-cms-sdk`](../python).
|
|
4
|
+
|
|
5
|
+
## Credentials (where they live)
|
|
6
|
+
|
|
7
|
+
CMS **base URL** and **API token** are **not** tool arguments. Put them in the MCP host config `env` block (e.g. Cursor `mcp.json` / Settings → MCP):
|
|
8
|
+
|
|
9
|
+
| Parameter | Env var | Required |
|
|
10
|
+
|-----------|---------|----------|
|
|
11
|
+
| CMS base URL | `BSC_BASE_URL` | yes |
|
|
12
|
+
| API token | `BSC_API_KEY` | yes |
|
|
13
|
+
| Auth style `api-key` or `bearer` | `BSC_AUTH_STYLE` | no (default `api-key`) |
|
|
14
|
+
|
|
15
|
+
Do not commit real tokens. Do not pass them as tool inputs (they would land in chat logs).
|
|
16
|
+
|
|
17
|
+
### Cursor example
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"block-style-cms": {
|
|
23
|
+
"command": "uvx",
|
|
24
|
+
"args": ["block-style-cms-mcp"],
|
|
25
|
+
"env": {
|
|
26
|
+
"BSC_BASE_URL": "https://cms.example.com",
|
|
27
|
+
"BSC_API_KEY": "your-api-token"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Local checkout (before PyPI):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"block-style-cms": {
|
|
40
|
+
"command": "uv",
|
|
41
|
+
"args": [
|
|
42
|
+
"run",
|
|
43
|
+
"--directory",
|
|
44
|
+
"/absolute/path/to/packages/mcp-python",
|
|
45
|
+
"block-style-cms-mcp"
|
|
46
|
+
],
|
|
47
|
+
"env": {
|
|
48
|
+
"BSC_BASE_URL": "https://cms.example.com",
|
|
49
|
+
"BSC_API_KEY": "your-api-token"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
| Tool | Purpose |
|
|
59
|
+
|------|---------|
|
|
60
|
+
| `list_locales` | Supported article languages |
|
|
61
|
+
| `list_channels` | Site channels |
|
|
62
|
+
| `upload_image` | Upload local image |
|
|
63
|
+
| `delete_image` | Delete library image by filename |
|
|
64
|
+
| `publish_from_markdown` | Create article from markdown |
|
|
65
|
+
| `update_from_markdown` | Update article from markdown (`id` required) |
|
|
66
|
+
| `get_article` | Get by slug + id (optional `include_md_content`) |
|
|
67
|
+
| `get_article_by_slug` | Get by slug + locale (optional `include_md_content`) |
|
|
68
|
+
| `list_articles` | List metadata with filters (`published` query: `published` \| `draft` \| `all`, **not** `true`/`false`) |
|
|
69
|
+
| `update_article_metadata` | Metadata-only update |
|
|
70
|
+
| `set_article_published` | Publish / unpublish |
|
|
71
|
+
| `delete_article` | Delete by slug + id |
|
|
72
|
+
| `delete_article_by_slug` | Delete by slug + locale |
|
|
73
|
+
|
|
74
|
+
Article closed-loop design: see repo [`docs/`](../../docs/README.md).
|
|
75
|
+
|
|
76
|
+
Path arguments (`md_file`, `assets_dir`, `file_path`, `cover_file`) must be readable by the MCP process (typically paths under the host workspace).
|
|
77
|
+
|
|
78
|
+
`list_articles.published` accepts only `published` | `draft` | `all` (CMS query enum). Boolean strings fail. Create/update and `set_article_published` still use a real boolean for the article’s published flag.
|
|
79
|
+
|
|
80
|
+
## Develop
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cd packages/mcp-python
|
|
84
|
+
uv sync --extra dev
|
|
85
|
+
uv run ruff check .
|
|
86
|
+
uv run pytest
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Release
|
|
90
|
+
|
|
91
|
+
1. Ensure the required `block-style-cms-sdk` version is already on PyPI.
|
|
92
|
+
2. Bump `version` in `pyproject.toml` (and keep the SDK dependency pin in sync).
|
|
93
|
+
3. Tag `mcp-py-vX.Y.Z`, push, and **Publish** a GitHub Release for that tag.
|
|
94
|
+
4. GitHub Actions uploads to PyPI via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC; GitHub Environment **`pypi-mcp`**, separate from the SDK’s `pypi` environment).
|
|
95
|
+
|
|
96
|
+
Example: version `0.0.3` → tag `mcp-py-v0.0.3` → GitHub Release → PyPI.
|
|
97
|
+
|
|
98
|
+
Setup checklist:
|
|
99
|
+
|
|
100
|
+
1. GitHub → Settings → Environments → create **`pypi-mcp`**.
|
|
101
|
+
2. Deployment branches and tags: allow tag pattern **`mcp-py-v*`** (optionally branch `master` for manual dry-runs).
|
|
102
|
+
3. On PyPI, register a Trusted Publisher for project `block-style-cms-mcp` pointing at workflow `mcp-python-release.yml` (and environment name `pypi-mcp` if the form asks for it).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "block-style-cms-mcp"
|
|
3
|
+
version = "0.0.3"
|
|
4
|
+
description = "MCP server for Block Style CMS (wraps block-style-cms-sdk)"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Biscuit Zhou", email = "biscuit_zhou@outlook.com" }
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
keywords = ["cms", "mcp", "blocknote", "markdown"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"block-style-cms-sdk>=0.0.3",
|
|
25
|
+
"mcp>=2.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
"ruff>=0.9",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
block-style-cms-mcp = "block_style_cms_mcp:main"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/block_style_cms_mcp"]
|
|
43
|
+
|
|
44
|
+
# Local monorepo only (not shipped in the PyPI package metadata).
|
|
45
|
+
# Published installs resolve block-style-cms-sdk>=0.0.3 from PyPI.
|
|
46
|
+
[tool.uv.sources]
|
|
47
|
+
block-style-cms-sdk = { path = "../python", editable = true }
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 120
|
|
51
|
+
target-version = "py310"
|
|
52
|
+
src = ["src", "tests"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.format]
|
|
58
|
+
quote-style = "double"
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Load CMS credentials from host-injected environment variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
AuthStyle = Literal["api-key", "bearer"]
|
|
10
|
+
|
|
11
|
+
ENV_BASE_URL = "BSC_BASE_URL"
|
|
12
|
+
ENV_API_KEY = "BSC_API_KEY"
|
|
13
|
+
ENV_AUTH_STYLE = "BSC_AUTH_STYLE"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Settings:
|
|
18
|
+
base_url: str
|
|
19
|
+
api_key: str
|
|
20
|
+
auth_style: AuthStyle = "api-key"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_settings(
|
|
24
|
+
environ: dict[str, str] | None = None,
|
|
25
|
+
) -> Settings:
|
|
26
|
+
"""Read BSC_* env vars. Raises SystemExit with a clear message if missing."""
|
|
27
|
+
env = environ if environ is not None else os.environ
|
|
28
|
+
missing: list[str] = []
|
|
29
|
+
base_url = (env.get(ENV_BASE_URL) or "").strip()
|
|
30
|
+
api_key = (env.get(ENV_API_KEY) or "").strip()
|
|
31
|
+
if not base_url:
|
|
32
|
+
missing.append(ENV_BASE_URL)
|
|
33
|
+
if not api_key:
|
|
34
|
+
missing.append(ENV_API_KEY)
|
|
35
|
+
if missing:
|
|
36
|
+
names = ", ".join(missing)
|
|
37
|
+
raise SystemExit(
|
|
38
|
+
f"Missing required environment variable(s): {names}. "
|
|
39
|
+
"Set them in the MCP host config env block "
|
|
40
|
+
"(e.g. Cursor mcp.json), not as tool arguments."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
raw_style = (env.get(ENV_AUTH_STYLE) or "api-key").strip().lower()
|
|
44
|
+
if raw_style not in ("api-key", "bearer"):
|
|
45
|
+
raise SystemExit(
|
|
46
|
+
f"Invalid {ENV_AUTH_STYLE}={raw_style!r}; expected 'api-key' or 'bearer'."
|
|
47
|
+
)
|
|
48
|
+
return Settings(
|
|
49
|
+
base_url=base_url,
|
|
50
|
+
api_key=api_key,
|
|
51
|
+
auth_style=raw_style, # type: ignore[arg-type]
|
|
52
|
+
)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""stdio MCP server wrapping BlockStyleCmsClient."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from block_style_cms_sdk import BlockStyleCmsClient
|
|
8
|
+
from block_style_cms_sdk.models import ArticleListPublishedFilter, ImageUploadOptions
|
|
9
|
+
from mcp.server.mcpserver import MCPServer
|
|
10
|
+
|
|
11
|
+
from .config import Settings, load_settings
|
|
12
|
+
from .tools import (
|
|
13
|
+
TOOL_DELETE_ARTICLE,
|
|
14
|
+
TOOL_DELETE_ARTICLE_BY_SLUG,
|
|
15
|
+
TOOL_DELETE_IMAGE,
|
|
16
|
+
TOOL_GET_ARTICLE,
|
|
17
|
+
TOOL_GET_ARTICLE_BY_SLUG,
|
|
18
|
+
TOOL_LIST_ARTICLES,
|
|
19
|
+
TOOL_LIST_CHANNELS,
|
|
20
|
+
TOOL_LIST_LOCALES,
|
|
21
|
+
TOOL_PUBLISH_FROM_MARKDOWN,
|
|
22
|
+
TOOL_SET_ARTICLE_PUBLISHED,
|
|
23
|
+
TOOL_UPDATE_ARTICLE_METADATA,
|
|
24
|
+
TOOL_UPDATE_FROM_MARKDOWN,
|
|
25
|
+
TOOL_UPLOAD_IMAGE,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_server(
|
|
30
|
+
client: BlockStyleCmsClient,
|
|
31
|
+
*,
|
|
32
|
+
name: str = "block-style-cms",
|
|
33
|
+
) -> MCPServer:
|
|
34
|
+
"""Build an MCP server bound to an existing SDK client (injectable for tests)."""
|
|
35
|
+
mcp = MCPServer(name)
|
|
36
|
+
|
|
37
|
+
@mcp.tool(name=TOOL_LIST_LOCALES)
|
|
38
|
+
def list_locales() -> list[dict[str, Any]]:
|
|
39
|
+
"""List supported article locales from the CMS."""
|
|
40
|
+
return [loc.model_dump() for loc in client.content.list_locales()]
|
|
41
|
+
|
|
42
|
+
@mcp.tool(name=TOOL_LIST_CHANNELS)
|
|
43
|
+
def list_channels() -> list[dict[str, Any]]:
|
|
44
|
+
"""List site channels from CMS settings."""
|
|
45
|
+
return [ch.model_dump() for ch in client.content.list_channels()]
|
|
46
|
+
|
|
47
|
+
@mcp.tool(name=TOOL_UPLOAD_IMAGE)
|
|
48
|
+
def upload_image(
|
|
49
|
+
file_path: str,
|
|
50
|
+
quality: int | None = None,
|
|
51
|
+
max_width: int | None = None,
|
|
52
|
+
max_height: int | None = None,
|
|
53
|
+
enable_webp: bool | None = None,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Upload a local image file to the CMS media library.
|
|
56
|
+
|
|
57
|
+
file_path must be readable from this MCP process (usually the host workspace).
|
|
58
|
+
"""
|
|
59
|
+
opts: ImageUploadOptions | None = None
|
|
60
|
+
if any(v is not None for v in (quality, max_width, max_height, enable_webp)):
|
|
61
|
+
opts = ImageUploadOptions(
|
|
62
|
+
quality=quality,
|
|
63
|
+
maxWidth=max_width,
|
|
64
|
+
maxHeight=max_height,
|
|
65
|
+
enableWebP=enable_webp,
|
|
66
|
+
)
|
|
67
|
+
meta = client.media.upload_image_file(file_path, opts)
|
|
68
|
+
return meta.model_dump()
|
|
69
|
+
|
|
70
|
+
@mcp.tool(name=TOOL_DELETE_IMAGE)
|
|
71
|
+
def delete_image(filename: str) -> Any:
|
|
72
|
+
"""Delete an image from the CMS media library by filename."""
|
|
73
|
+
return client.media.delete_image(filename)
|
|
74
|
+
|
|
75
|
+
@mcp.tool(name=TOOL_PUBLISH_FROM_MARKDOWN)
|
|
76
|
+
def publish_from_markdown(
|
|
77
|
+
title: str,
|
|
78
|
+
slug: str,
|
|
79
|
+
language: str,
|
|
80
|
+
assets_dir: str,
|
|
81
|
+
md_content: str | None = None,
|
|
82
|
+
md_file: str | None = None,
|
|
83
|
+
summary: str = "",
|
|
84
|
+
tags: list[str] | None = None,
|
|
85
|
+
keywords: list[str] | None = None,
|
|
86
|
+
cover: str = "",
|
|
87
|
+
cover_file: str | None = None,
|
|
88
|
+
published: bool = False,
|
|
89
|
+
locale_param: str | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""Publish an article from markdown, uploading relative local images.
|
|
92
|
+
|
|
93
|
+
language is a site language tag (e.g. zh-CN, en-US, ja-JP); call
|
|
94
|
+
list_locales first for values this CMS accepts. Provide md_content or
|
|
95
|
+
md_file. assets_dir is required to resolve relative image paths. Paths
|
|
96
|
+
are relative to the MCP process working directory / host workspace.
|
|
97
|
+
"""
|
|
98
|
+
result = client.articles.publish_from_markdown(
|
|
99
|
+
title=title,
|
|
100
|
+
slug=slug,
|
|
101
|
+
language=language,
|
|
102
|
+
assets_dir=assets_dir,
|
|
103
|
+
md_content=md_content,
|
|
104
|
+
md_file=md_file,
|
|
105
|
+
summary=summary,
|
|
106
|
+
tags=tags,
|
|
107
|
+
keywords=keywords,
|
|
108
|
+
cover=cover,
|
|
109
|
+
cover_file=cover_file,
|
|
110
|
+
published=published,
|
|
111
|
+
locale_param=locale_param,
|
|
112
|
+
)
|
|
113
|
+
return result.model_dump()
|
|
114
|
+
|
|
115
|
+
@mcp.tool(name=TOOL_UPDATE_FROM_MARKDOWN)
|
|
116
|
+
def update_from_markdown(
|
|
117
|
+
id: str,
|
|
118
|
+
title: str,
|
|
119
|
+
slug: str,
|
|
120
|
+
language: str,
|
|
121
|
+
assets_dir: str,
|
|
122
|
+
md_content: str | None = None,
|
|
123
|
+
md_file: str | None = None,
|
|
124
|
+
summary: str = "",
|
|
125
|
+
tags: list[str] | None = None,
|
|
126
|
+
keywords: list[str] | None = None,
|
|
127
|
+
cover: str = "",
|
|
128
|
+
cover_file: str | None = None,
|
|
129
|
+
published: bool = False,
|
|
130
|
+
locale_param: str | None = None,
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
"""Update an existing article from markdown (requires id).
|
|
133
|
+
|
|
134
|
+
Prefer editing mdContent from get_article so images stay absolute http(s)
|
|
135
|
+
URLs and uploads are skipped. Only relative local paths are uploaded.
|
|
136
|
+
"""
|
|
137
|
+
result = client.articles.update_from_markdown(
|
|
138
|
+
id=id,
|
|
139
|
+
title=title,
|
|
140
|
+
slug=slug,
|
|
141
|
+
language=language,
|
|
142
|
+
assets_dir=assets_dir,
|
|
143
|
+
md_content=md_content,
|
|
144
|
+
md_file=md_file,
|
|
145
|
+
summary=summary,
|
|
146
|
+
tags=tags,
|
|
147
|
+
keywords=keywords,
|
|
148
|
+
cover=cover,
|
|
149
|
+
cover_file=cover_file,
|
|
150
|
+
published=published,
|
|
151
|
+
locale_param=locale_param,
|
|
152
|
+
)
|
|
153
|
+
return result.model_dump()
|
|
154
|
+
|
|
155
|
+
@mcp.tool(name=TOOL_GET_ARTICLE)
|
|
156
|
+
def get_article(
|
|
157
|
+
slug: str,
|
|
158
|
+
id: str,
|
|
159
|
+
include_md_content: bool = False,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""Get full article by slug and id (BlockNote content by default).
|
|
162
|
+
|
|
163
|
+
Set include_md_content=true to also receive mdContent export.
|
|
164
|
+
"""
|
|
165
|
+
return client.articles.get(
|
|
166
|
+
slug, id, include_md_content=include_md_content
|
|
167
|
+
).model_dump()
|
|
168
|
+
|
|
169
|
+
@mcp.tool(name=TOOL_GET_ARTICLE_BY_SLUG)
|
|
170
|
+
def get_article_by_slug(
|
|
171
|
+
slug: str,
|
|
172
|
+
locale: str,
|
|
173
|
+
include_md_content: bool = False,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Get full article by slug and locale (BlockNote content by default).
|
|
176
|
+
|
|
177
|
+
Set include_md_content=true to also receive mdContent export.
|
|
178
|
+
"""
|
|
179
|
+
return client.articles.get_by_slug(
|
|
180
|
+
slug, locale, include_md_content=include_md_content
|
|
181
|
+
).model_dump()
|
|
182
|
+
|
|
183
|
+
@mcp.tool(name=TOOL_LIST_ARTICLES)
|
|
184
|
+
def list_articles(
|
|
185
|
+
locale: str | None = None,
|
|
186
|
+
tag: str | None = None,
|
|
187
|
+
search_term: str | None = None,
|
|
188
|
+
published: ArticleListPublishedFilter | None = None,
|
|
189
|
+
page_index: int | None = None,
|
|
190
|
+
page_size: int | None = None,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""List article metadata with optional filters.
|
|
193
|
+
|
|
194
|
+
published is a CMS status filter token, not a boolean:
|
|
195
|
+
"published" | "draft" | "all". Do not pass "true"/"false"
|
|
196
|
+
(the API rejects them). Omit or use "all" for no status filter.
|
|
197
|
+
This is unrelated to set_article_published / create-time
|
|
198
|
+
published:bool fields.
|
|
199
|
+
"""
|
|
200
|
+
return client.articles.list(
|
|
201
|
+
locale=locale,
|
|
202
|
+
tag=tag,
|
|
203
|
+
search_term=search_term,
|
|
204
|
+
published=published,
|
|
205
|
+
page_index=page_index,
|
|
206
|
+
page_size=page_size,
|
|
207
|
+
).model_dump()
|
|
208
|
+
|
|
209
|
+
@mcp.tool(name=TOOL_UPDATE_ARTICLE_METADATA)
|
|
210
|
+
def update_article_metadata(
|
|
211
|
+
id: str,
|
|
212
|
+
slug: str,
|
|
213
|
+
title: str,
|
|
214
|
+
locale: str,
|
|
215
|
+
summary: str = "",
|
|
216
|
+
tags: list[str] | None = None,
|
|
217
|
+
keywords: list[str] | None = None,
|
|
218
|
+
image: str = "",
|
|
219
|
+
published: bool | None = None,
|
|
220
|
+
locale_param: str | None = None,
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
"""Update article metadata without replacing content blocks."""
|
|
223
|
+
fields: dict[str, Any] = {
|
|
224
|
+
"id": id,
|
|
225
|
+
"slug": slug,
|
|
226
|
+
"title": title,
|
|
227
|
+
"locale": locale,
|
|
228
|
+
"summary": summary,
|
|
229
|
+
"tags": tags or [],
|
|
230
|
+
"keywords": keywords or [],
|
|
231
|
+
"image": image,
|
|
232
|
+
}
|
|
233
|
+
if published is not None:
|
|
234
|
+
fields["published"] = published
|
|
235
|
+
if locale_param:
|
|
236
|
+
fields["__locale__"] = locale_param
|
|
237
|
+
return client.articles.update_metadata(**fields)
|
|
238
|
+
|
|
239
|
+
@mcp.tool(name=TOOL_SET_ARTICLE_PUBLISHED)
|
|
240
|
+
def set_article_published(
|
|
241
|
+
id: str,
|
|
242
|
+
slug: str,
|
|
243
|
+
published: bool,
|
|
244
|
+
locale: str | None = None,
|
|
245
|
+
locale_param: str | None = None,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
"""Publish or unpublish an article (loads current metadata first)."""
|
|
248
|
+
return client.articles.set_published(
|
|
249
|
+
id=id,
|
|
250
|
+
slug=slug,
|
|
251
|
+
published=published,
|
|
252
|
+
locale=locale,
|
|
253
|
+
locale_param=locale_param,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@mcp.tool(name=TOOL_DELETE_ARTICLE)
|
|
257
|
+
def delete_article(slug: str, id: str) -> dict[str, Any]:
|
|
258
|
+
"""Delete an article by slug and id."""
|
|
259
|
+
return client.articles.delete(slug, id)
|
|
260
|
+
|
|
261
|
+
@mcp.tool(name=TOOL_DELETE_ARTICLE_BY_SLUG)
|
|
262
|
+
def delete_article_by_slug(slug: str, locale: str) -> dict[str, Any]:
|
|
263
|
+
"""Delete an article by slug and locale."""
|
|
264
|
+
return client.articles.delete_by_slug(slug, locale)
|
|
265
|
+
|
|
266
|
+
return mcp
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def build_client(settings: Settings) -> BlockStyleCmsClient:
|
|
270
|
+
return BlockStyleCmsClient(
|
|
271
|
+
base_url=settings.base_url,
|
|
272
|
+
api_key=settings.api_key,
|
|
273
|
+
auth_style=settings.auth_style,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main() -> None:
|
|
278
|
+
settings = load_settings()
|
|
279
|
+
client = build_client(settings)
|
|
280
|
+
try:
|
|
281
|
+
create_server(client).run()
|
|
282
|
+
finally:
|
|
283
|
+
client.close()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Tool names (schemas come from MCPServer handler signatures)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Public MCP tool names (snake_case; stable contract).
|
|
6
|
+
TOOL_LIST_LOCALES = "list_locales"
|
|
7
|
+
TOOL_LIST_CHANNELS = "list_channels"
|
|
8
|
+
TOOL_UPLOAD_IMAGE = "upload_image"
|
|
9
|
+
TOOL_DELETE_IMAGE = "delete_image"
|
|
10
|
+
TOOL_PUBLISH_FROM_MARKDOWN = "publish_from_markdown"
|
|
11
|
+
TOOL_UPDATE_FROM_MARKDOWN = "update_from_markdown"
|
|
12
|
+
TOOL_GET_ARTICLE = "get_article"
|
|
13
|
+
TOOL_GET_ARTICLE_BY_SLUG = "get_article_by_slug"
|
|
14
|
+
TOOL_LIST_ARTICLES = "list_articles"
|
|
15
|
+
TOOL_UPDATE_ARTICLE_METADATA = "update_article_metadata"
|
|
16
|
+
TOOL_SET_ARTICLE_PUBLISHED = "set_article_published"
|
|
17
|
+
TOOL_DELETE_ARTICLE = "delete_article"
|
|
18
|
+
TOOL_DELETE_ARTICLE_BY_SLUG = "delete_article_by_slug"
|
|
19
|
+
|
|
20
|
+
TOOL_NAMES = (
|
|
21
|
+
TOOL_LIST_LOCALES,
|
|
22
|
+
TOOL_LIST_CHANNELS,
|
|
23
|
+
TOOL_UPLOAD_IMAGE,
|
|
24
|
+
TOOL_DELETE_IMAGE,
|
|
25
|
+
TOOL_PUBLISH_FROM_MARKDOWN,
|
|
26
|
+
TOOL_UPDATE_FROM_MARKDOWN,
|
|
27
|
+
TOOL_GET_ARTICLE,
|
|
28
|
+
TOOL_GET_ARTICLE_BY_SLUG,
|
|
29
|
+
TOOL_LIST_ARTICLES,
|
|
30
|
+
TOOL_UPDATE_ARTICLE_METADATA,
|
|
31
|
+
TOOL_SET_ARTICLE_PUBLISHED,
|
|
32
|
+
TOOL_DELETE_ARTICLE,
|
|
33
|
+
TOOL_DELETE_ARTICLE_BY_SLUG,
|
|
34
|
+
)
|