n2n-post2site 0.1.2 → 0.1.3
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/CHANGELOG.md +19 -0
- package/README.md +51 -241
- package/assets/n2n-post2site-banner.jpg +0 -0
- package/assets/n2n-post2site-icon.png +0 -0
- package/assets/n2n-post2site-logo.png +0 -0
- package/dist/content-client.js +35 -16
- package/dist/index.js +2 -56
- package/dist/result.js +10 -0
- package/dist/schemas/blog-post.js +2 -0
- package/dist/server.js +28 -0
- package/dist/tools/create-post.js +10 -0
- package/dist/tools/get-capabilities.js +9 -0
- package/dist/tools/get-post.js +9 -0
- package/dist/tools/get-product-context.js +10 -0
- package/dist/tools/list-drafts.js +9 -0
- package/dist/tools/list-posts.js +9 -0
- package/dist/tools/publish-post.js +9 -0
- package/dist/tools/update-draft.js +10 -0
- package/dist/tools/update-post.js +10 -0
- package/dist/transport/http.js +28 -0
- package/docs/ARCHITECTURE.md +127 -0
- package/docs/BACKEND_API.md +111 -0
- package/docs/REFACTOR_PLAN.md +152 -0
- package/docs/TOOLS_REFERENCE.md +142 -0
- package/package.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to N2N Post2Site are documented in this file.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- None yet.
|
|
10
|
+
|
|
11
|
+
## [0.1.3] - 2026-06-17
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `n2n_list_drafts` for listing unpublished drafts through the existing posts endpoint with `status=draft`.
|
|
16
|
+
- Added `n2n_update_draft`, which reads the target post first and refuses to patch unless the backend reports `status: "draft"`.
|
|
17
|
+
- Added `docs/ARCHITECTURE.md` to document runtime layers, module boundaries, and request flow.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Added `docs/ARCHITECTURE.md` to the README related docs list.
|
|
22
|
+
- Bumped package version to `0.1.3`.
|
|
23
|
+
|
|
5
24
|
## [0.1.2] - 2026-06-15
|
|
6
25
|
|
|
7
26
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/n2n-post2site-logo.png" width="128" alt="n2n-post2site logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# n2n-post2site
|
|
2
6
|
|
|
3
7
|
AI-assisted content publishing MCP server for blogs and product guides.
|
|
4
8
|
|
|
@@ -9,37 +13,21 @@ AI-assisted content publishing MCP server for blogs and product guides.
|
|
|
9
13
|
[](https://nodejs.org)
|
|
10
14
|
[](https://datafrog.io)
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
It is intentionally small: a local MCP bridge between your IDE assistant and your website content API. Your website remains responsible for authentication, validation, storage, preview, publishing rules, and audit behavior.
|
|
15
|
-
|
|
16
|
-
## Quick summary
|
|
16
|
+
---
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
- Connects to one website content API through `CONTENT_API_BASE_URL` and `CONTENT_API_KEY`.
|
|
20
|
-
- Creates drafts by default.
|
|
21
|
-
- Publishes only through an explicit publish tool.
|
|
22
|
-
- Submits one locale per create or update call.
|
|
23
|
-
- Uses Markdown-first content, with optional inline HTML when your site supports it.
|
|
24
|
-
- Reads product context before drafting product guides.
|
|
25
|
-
- Does not expose database, shell, server, payment, user, pricing, or delete operations.
|
|
18
|
+
> **Draft with AI. Publish with intent.**
|
|
26
19
|
|
|
27
|
-
|
|
20
|
+
n2n-post2site is an open-source Model Context Protocol (MCP) server that lets an AI assistant draft, edit, review, and publish website content through a narrow Content Publishing API Contract. It is a local MCP bridge between your IDE assistant and your website content API — intentionally small, with no database access, shell access, deployment access, payment access, or user administration access.
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
## 💡 What is n2n-post2site?
|
|
30
23
|
|
|
31
|
-
-
|
|
32
|
-
- Product guides.
|
|
33
|
-
- Technical field notes.
|
|
34
|
-
- Release notes and changelogs.
|
|
35
|
-
- Localized article drafts.
|
|
36
|
-
- Safe updates to existing website content.
|
|
24
|
+
n2n-post2site gives AI coding assistants a structured path to draft and publish website content without exposing the database, file system, or deployment layer. Your backend implements the Content Publishing API Contract; the MCP server forwards AI tool calls to it.
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
Use it for blog posts, product guides, technical notes, release notes, and localized article drafts. It is not a CMS — no admin panel, no storage backend, no image uploads (v1), no deployment workflow.
|
|
39
27
|
|
|
40
|
-
## Publishing model
|
|
28
|
+
## 📰 Publishing model
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
n2n-post2site works with two publishing spaces:
|
|
43
31
|
|
|
44
32
|
| Space | `content_scope` | Example |
|
|
45
33
|
|---|---|---|
|
|
@@ -54,50 +42,17 @@ The assistant should follow this workflow:
|
|
|
54
42
|
2. Search existing content with `n2n_list_posts`.
|
|
55
43
|
3. For product guides, call `n2n_get_product_context`.
|
|
56
44
|
4. Create or update one locale at a time.
|
|
57
|
-
5.
|
|
58
|
-
6.
|
|
59
|
-
|
|
60
|
-
## Requirements
|
|
61
|
-
|
|
62
|
-
- Node.js 22+
|
|
63
|
-
- An MCP-capable client or IDE
|
|
64
|
-
- A site-scoped content API token
|
|
65
|
-
- A backend that implements the Content Publishing API Contract
|
|
66
|
-
|
|
67
|
-
## Install
|
|
68
|
-
|
|
69
|
-
For local development:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npm install
|
|
73
|
-
npm run build
|
|
74
|
-
npm run check
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
For MCP clients, use the published package after it is available on npm:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
npx -y n2n-post2site
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Configuration
|
|
84
|
-
|
|
85
|
-
Set these environment variables in your MCP client configuration:
|
|
86
|
-
|
|
87
|
-
```env
|
|
88
|
-
CONTENT_API_BASE_URL=https://example.com/api/v1/mcp
|
|
89
|
-
CONTENT_API_KEY=change-me
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
`CONTENT_API_BASE_URL` is the base URL of your protected content API. The backend should expose `/capabilities`, `/products/{content_scope}`, and `/posts` relative to this base URL.
|
|
45
|
+
5. Resume unfinished work with `n2n_list_drafts`, `n2n_get_post`, and `n2n_update_draft`.
|
|
46
|
+
6. Review the draft.
|
|
47
|
+
7. Publish only through `n2n_publish_post`.
|
|
93
48
|
|
|
94
|
-
|
|
49
|
+
## 🚀 Quick start
|
|
95
50
|
|
|
96
|
-
|
|
51
|
+
**Requirements**: Node.js 22+, an MCP-capable client, a site-scoped API key, and a backend that implements the [Content Publishing API Contract](./docs/BACKEND_API.md).
|
|
97
52
|
|
|
98
|
-
|
|
53
|
+
### 1. Configure your MCP client
|
|
99
54
|
|
|
100
|
-
|
|
55
|
+
Using the npm package:
|
|
101
56
|
|
|
102
57
|
```json
|
|
103
58
|
{
|
|
@@ -106,15 +61,15 @@ Do not put API keys in prompts, article content, README examples, or screenshots
|
|
|
106
61
|
"command": "npx",
|
|
107
62
|
"args": ["-y", "n2n-post2site"],
|
|
108
63
|
"env": {
|
|
109
|
-
"CONTENT_API_BASE_URL": "https://
|
|
110
|
-
"CONTENT_API_KEY": "
|
|
64
|
+
"CONTENT_API_BASE_URL": "https://your-site.com/api/v1/mcp",
|
|
65
|
+
"CONTENT_API_KEY": "your-api-key"
|
|
111
66
|
}
|
|
112
67
|
}
|
|
113
68
|
}
|
|
114
69
|
}
|
|
115
70
|
```
|
|
116
71
|
|
|
117
|
-
|
|
72
|
+
Using a local checkout:
|
|
118
73
|
|
|
119
74
|
```json
|
|
120
75
|
{
|
|
@@ -123,185 +78,34 @@ Do not put API keys in prompts, article content, README examples, or screenshots
|
|
|
123
78
|
"command": "node",
|
|
124
79
|
"args": ["/path/to/n2n-post2site/dist/index.js"],
|
|
125
80
|
"env": {
|
|
126
|
-
"CONTENT_API_BASE_URL": "https://
|
|
127
|
-
"CONTENT_API_KEY": "
|
|
81
|
+
"CONTENT_API_BASE_URL": "https://your-site.com/api/v1/mcp",
|
|
82
|
+
"CONTENT_API_KEY": "your-api-key"
|
|
128
83
|
}
|
|
129
84
|
}
|
|
130
85
|
}
|
|
131
86
|
}
|
|
132
87
|
```
|
|
133
88
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## Backend API contract
|
|
137
|
-
|
|
138
|
-
The backend should support these endpoints relative to `CONTENT_API_BASE_URL`:
|
|
139
|
-
|
|
140
|
-
```http
|
|
141
|
-
GET /capabilities
|
|
142
|
-
GET /products/{content_scope}
|
|
143
|
-
GET /posts
|
|
144
|
-
POST /posts
|
|
145
|
-
GET /posts/{id_or_slug}
|
|
146
|
-
PATCH /posts/{id_or_slug}
|
|
147
|
-
POST /posts/{id_or_slug}/publish
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### `GET /capabilities`
|
|
151
|
-
|
|
152
|
-
Returns the publishing contract for AI clients, including:
|
|
153
|
-
|
|
154
|
-
- Supported content types.
|
|
155
|
-
- Supported statuses.
|
|
156
|
-
- Supported locales.
|
|
157
|
-
- Single-locale input fields.
|
|
158
|
-
- `content_scope` rules.
|
|
159
|
-
- Available product guide scopes.
|
|
160
|
-
- Safety boundaries.
|
|
161
|
-
|
|
162
|
-
### `GET /products/{content_scope}`
|
|
163
|
-
|
|
164
|
-
Returns controlled product context before drafting product guides.
|
|
165
|
-
|
|
166
|
-
Expected fields:
|
|
167
|
-
|
|
168
|
-
- `content_scope`: confirms the valid product guide scope.
|
|
169
|
-
- `canonical_url`: product page for deeper reading, links, and citations.
|
|
170
|
-
- `docs_url`: docs or guide index to prefer for tutorials.
|
|
171
|
-
- `summary`: controlled product summary.
|
|
172
|
-
- `key_points`: controlled facts the assistant may rely on.
|
|
173
|
-
- `do_not_claim`: claims the assistant must not make.
|
|
174
|
-
|
|
175
|
-
### Create and update payloads
|
|
176
|
-
|
|
177
|
-
Create and update calls use one locale per request:
|
|
178
|
-
|
|
179
|
-
```json
|
|
180
|
-
{
|
|
181
|
-
"slug": "example-product-guide",
|
|
182
|
-
"type": "guide",
|
|
183
|
-
"content_scope": "product:example-product",
|
|
184
|
-
"locale": "en",
|
|
185
|
-
"title": "How to Use Example Product",
|
|
186
|
-
"excerpt": "A short guide to using the example product.",
|
|
187
|
-
"content": "## Overview\n\nMarkdown content..."
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Rules:
|
|
89
|
+
- `CONTENT_API_BASE_URL`: base URL of your protected content API. Keep path and field mapping in the backend adapter.
|
|
90
|
+
- `CONTENT_API_KEY`: site-scoped API key. Do not put it in prompts, article content, or screenshots.
|
|
192
91
|
|
|
193
|
-
|
|
194
|
-
- `excerpt` is plain text.
|
|
195
|
-
- `content` is Markdown.
|
|
196
|
-
- Inline HTML is allowed when useful.
|
|
197
|
-
- Full HTML documents with `<html>`, `<head>`, or `<body>` are not allowed.
|
|
198
|
-
- Create/update must not accept `status`, `published_at`, `user_id`, or `author`.
|
|
199
|
-
- Publishing state changes only through `/posts/{id_or_slug}/publish`.
|
|
92
|
+
Bind one server instance to exactly one website.
|
|
200
93
|
|
|
201
|
-
|
|
94
|
+
## 🔗 Backend API contract
|
|
202
95
|
|
|
203
|
-
|
|
96
|
+
n2n-post2site connects to any backend that implements the Content Publishing API Contract. The contract defines the required endpoints, payload rules, and security requirements.
|
|
204
97
|
|
|
205
|
-
|
|
98
|
+
See **[Backend API Contract](./docs/BACKEND_API.md)** for the full specification.
|
|
206
99
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
```json
|
|
210
|
-
{}
|
|
211
|
-
```
|
|
100
|
+
## 🛠️ MCP tools
|
|
212
101
|
|
|
213
|
-
|
|
102
|
+
- **Discovery**: read backend capabilities, list existing posts, and load product context before drafting.
|
|
103
|
+
- **Drafting**: create posts, update posts one locale at a time, and resume unpublished drafts.
|
|
104
|
+
- **Publishing**: publish an approved draft through an explicit publish step.
|
|
214
105
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
```json
|
|
218
|
-
{
|
|
219
|
-
"status": "draft",
|
|
220
|
-
"type": "guide",
|
|
221
|
-
"content_scope": "product:example-product",
|
|
222
|
-
"q": "setup guide",
|
|
223
|
-
"per_page": 20
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
`status` is only a filter here. Do not send `status` in create or update calls.
|
|
228
|
-
|
|
229
|
-
### `n2n_get_post`
|
|
230
|
-
|
|
231
|
-
Read an existing post before updating it, completing missing locales, or writing a follow-up.
|
|
232
|
-
|
|
233
|
-
```json
|
|
234
|
-
{
|
|
235
|
-
"id_or_slug": "example-product-guide"
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### `n2n_get_product_context`
|
|
240
|
-
|
|
241
|
-
Read controlled product facts before drafting a product guide.
|
|
242
|
-
|
|
243
|
-
```json
|
|
244
|
-
{
|
|
245
|
-
"content_scope": "product:example-product"
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### `n2n_create_post`
|
|
250
|
-
|
|
251
|
-
Create a draft. Publishing is separate.
|
|
252
|
-
|
|
253
|
-
Company blog example:
|
|
254
|
-
|
|
255
|
-
```json
|
|
256
|
-
{
|
|
257
|
-
"slug": "content-workflow-notes",
|
|
258
|
-
"type": "technical",
|
|
259
|
-
"locale": "en",
|
|
260
|
-
"title": "Content Workflow Notes",
|
|
261
|
-
"excerpt": "Practical notes from shipping a content workflow.",
|
|
262
|
-
"content": "## Notes\n\nMarkdown content..."
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Product guide example:
|
|
267
|
-
|
|
268
|
-
```json
|
|
269
|
-
{
|
|
270
|
-
"slug": "example-product-guide",
|
|
271
|
-
"type": "guide",
|
|
272
|
-
"content_scope": "product:example-product",
|
|
273
|
-
"locale": "en",
|
|
274
|
-
"title": "How to Use Example Product",
|
|
275
|
-
"excerpt": "A short guide to using the example product.",
|
|
276
|
-
"content": "## Overview\n\nMarkdown content..."
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### `n2n_update_post`
|
|
281
|
-
|
|
282
|
-
Update one locale of an existing post. Call `n2n_get_post` first.
|
|
283
|
-
|
|
284
|
-
```json
|
|
285
|
-
{
|
|
286
|
-
"id_or_slug": "example-product-guide",
|
|
287
|
-
"locale": "de",
|
|
288
|
-
"title": "So verwenden Sie Example Product",
|
|
289
|
-
"excerpt": "A localized summary for the selected locale.",
|
|
290
|
-
"content": "## Ueberblick\n\nLocalized Markdown content..."
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### `n2n_publish_post`
|
|
295
|
-
|
|
296
|
-
Publish an existing draft.
|
|
297
|
-
|
|
298
|
-
```json
|
|
299
|
-
{
|
|
300
|
-
"id_or_slug": "example-product-guide"
|
|
301
|
-
}
|
|
302
|
-
```
|
|
106
|
+
See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call examples.
|
|
303
107
|
|
|
304
|
-
## Content format
|
|
108
|
+
## 📝 Content format
|
|
305
109
|
|
|
306
110
|
- Submit one locale per create or update call.
|
|
307
111
|
- Use Markdown for `content`.
|
|
@@ -317,9 +121,9 @@ Image example:
|
|
|
317
121
|

|
|
318
122
|
```
|
|
319
123
|
|
|
320
|
-
##
|
|
124
|
+
## 🔐 Security and governance notes
|
|
321
125
|
|
|
322
|
-
|
|
126
|
+
n2n-post2site should not expose:
|
|
323
127
|
|
|
324
128
|
- Delete operations.
|
|
325
129
|
- Product configuration writes.
|
|
@@ -339,14 +143,20 @@ Recommended backend behavior:
|
|
|
339
143
|
- Keep create/update and publish separate.
|
|
340
144
|
- Set `published_at` on the backend, not from MCP input.
|
|
341
145
|
|
|
342
|
-
##
|
|
146
|
+
## 📖 Related docs
|
|
343
147
|
|
|
344
|
-
|
|
148
|
+
- **[Backend API Contract](./docs/BACKEND_API.md)**: Endpoints, payload rules, and security requirements for backend implementors.
|
|
149
|
+
- **[Architecture](./docs/ARCHITECTURE.md)**: Runtime layers, module boundaries, and request flow in the MCP server.
|
|
150
|
+
- **[Tools reference](./docs/TOOLS_REFERENCE.md)**: MCP tool parameter schemas and call examples.
|
|
151
|
+
- **[Roadmap](./ROADMAP.md)**: Planned features and what's coming next.
|
|
152
|
+
- **[Changelog](./CHANGELOG.md)**: Version history and release notes.
|
|
153
|
+
- **[Contributing](./CONTRIBUTING.md)**: How to report issues and contribute.
|
|
154
|
+
- **[Security](./SECURITY.md)**: How to report vulnerabilities.
|
|
345
155
|
|
|
346
|
-
##
|
|
156
|
+
## 📄 License
|
|
347
157
|
|
|
348
|
-
|
|
158
|
+
This project is licensed under the [MIT License](./LICENSE).
|
|
349
159
|
|
|
350
|
-
|
|
160
|
+
---
|
|
351
161
|
|
|
352
|
-
|
|
162
|
+
Built by [N2NS Lab](https://n2ns.com), the open-source lab of [datafrog.io](https://datafrog.io) for practical AI developer tools.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/content-client.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { FetchHttpTransport } from './transport/http.js';
|
|
1
2
|
const POSTS_PATH = '/posts';
|
|
2
3
|
const CAPABILITIES_PATH = '/capabilities';
|
|
3
4
|
const PRODUCTS_PATH = '/products';
|
|
4
5
|
export class ContentClient {
|
|
5
6
|
config;
|
|
6
|
-
|
|
7
|
+
transport;
|
|
8
|
+
constructor(config, transport = new FetchHttpTransport(config)) {
|
|
7
9
|
this.config = config;
|
|
10
|
+
this.transport = transport;
|
|
8
11
|
}
|
|
9
12
|
async getCapabilities() {
|
|
10
13
|
return this.request(CAPABILITIES_PATH, { method: 'GET' });
|
|
@@ -12,13 +15,17 @@ export class ContentClient {
|
|
|
12
15
|
async listPosts(input) {
|
|
13
16
|
const params = new URLSearchParams();
|
|
14
17
|
for (const [key, value] of Object.entries(input)) {
|
|
15
|
-
if (value === undefined)
|
|
18
|
+
if (value === undefined) {
|
|
16
19
|
continue;
|
|
20
|
+
}
|
|
17
21
|
params.set(key, String(value));
|
|
18
22
|
}
|
|
19
23
|
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
20
24
|
return this.request(`${POSTS_PATH}${suffix}`, { method: 'GET' });
|
|
21
25
|
}
|
|
26
|
+
async listDrafts(input) {
|
|
27
|
+
return this.listPosts({ ...input, status: 'draft' });
|
|
28
|
+
}
|
|
22
29
|
async getPost(idOrSlug) {
|
|
23
30
|
return this.request(`${POSTS_PATH}/${encodeURIComponent(idOrSlug)}`, { method: 'GET' });
|
|
24
31
|
}
|
|
@@ -38,13 +45,18 @@ export class ContentClient {
|
|
|
38
45
|
body: JSON.stringify(payload),
|
|
39
46
|
});
|
|
40
47
|
}
|
|
48
|
+
async updateDraft(input) {
|
|
49
|
+
const existingPost = await this.getPost(input.id_or_slug);
|
|
50
|
+
assertDraftStatus(existingPost);
|
|
51
|
+
return this.updatePost(input);
|
|
52
|
+
}
|
|
41
53
|
async publishPost(idOrSlug) {
|
|
42
54
|
return this.request(`${POSTS_PATH}/${encodeURIComponent(idOrSlug)}/publish`, {
|
|
43
55
|
method: 'POST',
|
|
44
56
|
});
|
|
45
57
|
}
|
|
46
58
|
async request(path, init) {
|
|
47
|
-
const response = await
|
|
59
|
+
const response = await this.transport.request(path, {
|
|
48
60
|
...init,
|
|
49
61
|
headers: {
|
|
50
62
|
Accept: 'application/json',
|
|
@@ -54,24 +66,31 @@ export class ContentClient {
|
|
|
54
66
|
...(init.headers ?? {}),
|
|
55
67
|
},
|
|
56
68
|
});
|
|
57
|
-
const text = await response.text();
|
|
58
|
-
const body = parseJsonOrText(text);
|
|
59
69
|
if (!response.ok) {
|
|
60
|
-
throw new Error(`Content API request failed: ${response.status} ${response.statusText}: ${formatBody(body)}`);
|
|
70
|
+
throw new Error(`Content API request failed: ${response.status} ${response.statusText}: ${formatBody(response.body)}`);
|
|
61
71
|
}
|
|
62
|
-
return body;
|
|
72
|
+
return response.body;
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
function formatBody(body) {
|
|
76
|
+
return typeof body === 'string' ? body : JSON.stringify(body, null, 2);
|
|
77
|
+
}
|
|
78
|
+
function assertDraftStatus(body) {
|
|
79
|
+
const status = extractPostStatus(body);
|
|
80
|
+
if (status === 'draft') {
|
|
81
|
+
return;
|
|
70
82
|
}
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
if (status) {
|
|
84
|
+
throw new Error(`n2n_update_draft requires a draft post. Current status: ${status}.`);
|
|
73
85
|
}
|
|
86
|
+
throw new Error('n2n_update_draft could not verify draft status. The backend GET /posts/{id_or_slug} response must include a status field.');
|
|
74
87
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
88
|
+
function extractPostStatus(body) {
|
|
89
|
+
const record = asRecord(body);
|
|
90
|
+
const post = asRecord(record?.blog_post) ?? asRecord(record?.data) ?? record;
|
|
91
|
+
const status = post?.status;
|
|
92
|
+
return typeof status === 'string' ? status : undefined;
|
|
93
|
+
}
|
|
94
|
+
function asRecord(value) {
|
|
95
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
77
96
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,62 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
3
|
import { loadConfig } from './config.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const server = new McpServer({
|
|
8
|
-
name: 'n2n-post2site',
|
|
9
|
-
version: '0.1.2',
|
|
10
|
-
});
|
|
11
|
-
const client = new ContentClient(loadConfig());
|
|
12
|
-
server.tool('n2n_get_capabilities', 'Read the backend Content Publishing API Contract before creating or updating content. Use this to discover supported content types, locales, content_scope rules, and product guide scopes.', capabilitiesSchema.shape, async (input) => {
|
|
13
|
-
capabilitiesSchema.parse(input);
|
|
14
|
-
const result = await client.getCapabilities();
|
|
15
|
-
return textResult(result);
|
|
16
|
-
});
|
|
17
|
-
server.tool('n2n_list_posts', 'Search and list existing website articles or product guides before drafting new content. Use this to avoid duplicate topics and conflicting guidance. For product guides, filter by content_scope. Use content_scope="" for unscoped company blog posts when the backend supports it.', listPostsSchema.shape, async (input) => {
|
|
18
|
-
const parsed = listPostsSchema.parse(input);
|
|
19
|
-
const result = await client.listPosts(parsed);
|
|
20
|
-
return textResult(result);
|
|
21
|
-
});
|
|
22
|
-
server.tool('n2n_get_post', 'Read one existing article or guide by numeric ID or slug. Use this before updating a post, completing missing locales, or writing a follow-up article that depends on previous content.', getPostSchema.shape, async (input) => {
|
|
23
|
-
const parsed = getPostSchema.parse(input);
|
|
24
|
-
const result = await client.getPost(parsed.id_or_slug);
|
|
25
|
-
return textResult(result);
|
|
26
|
-
});
|
|
27
|
-
server.tool('n2n_get_product_context', 'Read the controlled product fact sheet before writing a product guide. The backend returns content_scope, canonical_url, docs_url, summary, key_points, and do_not_claim so the article does not invent product facts.', productContextSchema.shape, async (input) => {
|
|
28
|
-
const parsed = productContextSchema.parse(input);
|
|
29
|
-
assertContentPostShape({ type: 'guide', content_scope: parsed.content_scope });
|
|
30
|
-
const result = await client.getProductContext(parsed);
|
|
31
|
-
return textResult(result);
|
|
32
|
-
});
|
|
33
|
-
server.tool('n2n_create_post', 'Create an article draft or product guide draft with one locale per call. Before creating new content, search existing posts with n2n_list_posts to avoid duplicates. Before creating a product guide, also call n2n_get_product_context for the target content_scope and follow its canonical_url, docs_url, summary, key_points, and do_not_claim. content must be a Markdown document string; inline HTML is allowed when useful. title and excerpt must be plain text. The backend returns missing_locales when more language versions should be added. Use n2n_publish_post to publish.', createPostSchema.shape, async (input) => {
|
|
34
|
-
const parsed = createPostSchema.parse(input);
|
|
35
|
-
assertContentPostShape(parsed);
|
|
36
|
-
const result = await client.createPost(parsed);
|
|
37
|
-
return textResult(result);
|
|
38
|
-
});
|
|
39
|
-
server.tool('n2n_update_post', 'Update one locale of an existing article or guide by ID or slug. Always call n2n_get_post first so edits preserve existing content and metadata. content must be a Markdown document string; inline HTML is allowed when useful. title and excerpt must be plain text. Use repeated calls with different locale values to add missing language versions.', updatePostSchema.shape, async (input) => {
|
|
40
|
-
const parsed = updatePostSchema.parse(input);
|
|
41
|
-
assertContentPostShape(parsed);
|
|
42
|
-
const result = await client.updatePost(parsed);
|
|
43
|
-
return textResult(result);
|
|
44
|
-
});
|
|
45
|
-
server.tool('n2n_publish_post', 'Publish an existing article or guide by ID or slug. Publishing is intentionally separate from create/update to avoid accidental publication.', publishPostSchema.shape, async (input) => {
|
|
46
|
-
const parsed = publishPostSchema.parse(input);
|
|
47
|
-
const result = await client.publishPost(parsed.id_or_slug);
|
|
48
|
-
return textResult(result);
|
|
49
|
-
});
|
|
50
|
-
function textResult(value) {
|
|
51
|
-
return {
|
|
52
|
-
content: [
|
|
53
|
-
{
|
|
54
|
-
type: 'text',
|
|
55
|
-
text: JSON.stringify(value, null, 2),
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
4
|
+
import { createServer } from './server.js';
|
|
5
|
+
const server = createServer(loadConfig());
|
|
60
6
|
async function main() {
|
|
61
7
|
const transport = new StdioServerTransport();
|
|
62
8
|
await server.connect(transport);
|
package/dist/result.js
ADDED
|
@@ -10,6 +10,7 @@ export const listPostsSchema = z.object({
|
|
|
10
10
|
q: z.string().optional().describe('Search query across title, content, and translations.'),
|
|
11
11
|
per_page: z.number().int().min(1).max(100).optional().describe('Page size. Defaults to the server value.'),
|
|
12
12
|
});
|
|
13
|
+
export const listDraftsSchema = listPostsSchema.omit({ status: true });
|
|
13
14
|
export const getPostSchema = z.object({
|
|
14
15
|
id_or_slug: z.string().min(1).describe('Numeric ID or slug of the article.'),
|
|
15
16
|
});
|
|
@@ -29,6 +30,7 @@ export const createPostSchema = z.object({
|
|
|
29
30
|
export const updatePostSchema = createPostSchema.partial().extend({
|
|
30
31
|
id_or_slug: z.string().min(1).describe('Numeric ID or slug of the article to update.'),
|
|
31
32
|
});
|
|
33
|
+
export const updateDraftSchema = updatePostSchema;
|
|
32
34
|
export const publishPostSchema = getPostSchema;
|
|
33
35
|
export function assertContentPostShape(input) {
|
|
34
36
|
const type = input.type ?? 'technical';
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { ContentClient } from './content-client.js';
|
|
3
|
+
import { registerCreatePostTool } from './tools/create-post.js';
|
|
4
|
+
import { registerGetCapabilitiesTool } from './tools/get-capabilities.js';
|
|
5
|
+
import { registerGetPostTool } from './tools/get-post.js';
|
|
6
|
+
import { registerGetProductContextTool } from './tools/get-product-context.js';
|
|
7
|
+
import { registerListDraftsTool } from './tools/list-drafts.js';
|
|
8
|
+
import { registerListPostsTool } from './tools/list-posts.js';
|
|
9
|
+
import { registerPublishPostTool } from './tools/publish-post.js';
|
|
10
|
+
import { registerUpdateDraftTool } from './tools/update-draft.js';
|
|
11
|
+
import { registerUpdatePostTool } from './tools/update-post.js';
|
|
12
|
+
export function createServer(config) {
|
|
13
|
+
const client = new ContentClient(config);
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: 'n2n-post2site',
|
|
16
|
+
version: '0.1.2',
|
|
17
|
+
});
|
|
18
|
+
registerGetCapabilitiesTool(server, client);
|
|
19
|
+
registerListPostsTool(server, client);
|
|
20
|
+
registerListDraftsTool(server, client);
|
|
21
|
+
registerGetPostTool(server, client);
|
|
22
|
+
registerGetProductContextTool(server, client);
|
|
23
|
+
registerCreatePostTool(server, client);
|
|
24
|
+
registerUpdatePostTool(server, client);
|
|
25
|
+
registerUpdateDraftTool(server, client);
|
|
26
|
+
registerPublishPostTool(server, client);
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { assertContentPostShape, createPostSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerCreatePostTool(server, client) {
|
|
4
|
+
server.tool('n2n_create_post', 'Create an article draft or product guide draft with one locale per call. Before creating new content, search existing posts with n2n_list_posts to avoid duplicates. Before creating a product guide, also call n2n_get_product_context for the target content_scope and follow its canonical_url, docs_url, summary, key_points, and do_not_claim. content must be a Markdown document string; inline HTML is allowed when useful. title and excerpt must be plain text. The backend returns missing_locales when more language versions should be added. Use n2n_publish_post to publish.', createPostSchema.shape, async (input) => {
|
|
5
|
+
const parsed = createPostSchema.parse(input);
|
|
6
|
+
assertContentPostShape(parsed);
|
|
7
|
+
const result = await client.createPost(parsed);
|
|
8
|
+
return createTextResult(result);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { capabilitiesSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerGetCapabilitiesTool(server, client) {
|
|
4
|
+
server.tool('n2n_get_capabilities', 'Read the backend Content Publishing API Contract before creating or updating content. Use this to discover supported content types, locales, content_scope rules, and product guide scopes.', capabilitiesSchema.shape, async (input) => {
|
|
5
|
+
capabilitiesSchema.parse(input);
|
|
6
|
+
const result = await client.getCapabilities();
|
|
7
|
+
return createTextResult(result);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { getPostSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerGetPostTool(server, client) {
|
|
4
|
+
server.tool('n2n_get_post', 'Read one existing article or guide by numeric ID or slug. Use this before updating a post, completing missing locales, or writing a follow-up article that depends on previous content.', getPostSchema.shape, async (input) => {
|
|
5
|
+
const parsed = getPostSchema.parse(input);
|
|
6
|
+
const result = await client.getPost(parsed.id_or_slug);
|
|
7
|
+
return createTextResult(result);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { assertContentPostShape, productContextSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerGetProductContextTool(server, client) {
|
|
4
|
+
server.tool('n2n_get_product_context', 'Read the controlled product fact sheet before writing a product guide. The backend returns content_scope, canonical_url, docs_url, summary, key_points, and do_not_claim so the article does not invent product facts.', productContextSchema.shape, async (input) => {
|
|
5
|
+
const parsed = productContextSchema.parse(input);
|
|
6
|
+
assertContentPostShape({ type: 'guide', content_scope: parsed.content_scope });
|
|
7
|
+
const result = await client.getProductContext(parsed);
|
|
8
|
+
return createTextResult(result);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { listDraftsSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerListDraftsTool(server, client) {
|
|
4
|
+
server.tool('n2n_list_drafts', 'List unpublished draft articles or product guides. Use this before resuming previous AI-written drafts. This tool always filters status=draft; use type, content_scope, q, and per_page to narrow the results.', listDraftsSchema.shape, async (input) => {
|
|
5
|
+
const parsed = listDraftsSchema.parse(input);
|
|
6
|
+
const result = await client.listDrafts(parsed);
|
|
7
|
+
return createTextResult(result);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { listPostsSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerListPostsTool(server, client) {
|
|
4
|
+
server.tool('n2n_list_posts', 'Search and list existing website articles or product guides before drafting new content. Use this to avoid duplicate topics and conflicting guidance. For product guides, filter by content_scope. Use content_scope="" for unscoped company blog posts when the backend supports it.', listPostsSchema.shape, async (input) => {
|
|
5
|
+
const parsed = listPostsSchema.parse(input);
|
|
6
|
+
const result = await client.listPosts(parsed);
|
|
7
|
+
return createTextResult(result);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { publishPostSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerPublishPostTool(server, client) {
|
|
4
|
+
server.tool('n2n_publish_post', 'Publish an existing article or guide by ID or slug. Publishing is intentionally separate from create/update to avoid accidental publication.', publishPostSchema.shape, async (input) => {
|
|
5
|
+
const parsed = publishPostSchema.parse(input);
|
|
6
|
+
const result = await client.publishPost(parsed.id_or_slug);
|
|
7
|
+
return createTextResult(result);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { assertContentPostShape, updateDraftSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerUpdateDraftTool(server, client) {
|
|
4
|
+
server.tool('n2n_update_draft', 'Update one locale of an unpublished draft by ID or slug. The client reads the post first and refuses to patch unless the backend reports status=draft. Use n2n_list_drafts and n2n_get_post before calling this tool.', updateDraftSchema.shape, async (input) => {
|
|
5
|
+
const parsed = updateDraftSchema.parse(input);
|
|
6
|
+
assertContentPostShape(parsed);
|
|
7
|
+
const result = await client.updateDraft(parsed);
|
|
8
|
+
return createTextResult(result);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createTextResult } from '../result.js';
|
|
2
|
+
import { assertContentPostShape, updatePostSchema } from '../schemas/blog-post.js';
|
|
3
|
+
export function registerUpdatePostTool(server, client) {
|
|
4
|
+
server.tool('n2n_update_post', 'Update one locale of an existing article or guide by ID or slug. Always call n2n_get_post first so edits preserve existing content and metadata. content must be a Markdown document string; inline HTML is allowed when useful. title and excerpt must be plain text. Use repeated calls with different locale values to add missing language versions.', updatePostSchema.shape, async (input) => {
|
|
5
|
+
const parsed = updatePostSchema.parse(input);
|
|
6
|
+
assertContentPostShape(parsed);
|
|
7
|
+
const result = await client.updatePost(parsed);
|
|
8
|
+
return createTextResult(result);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class FetchHttpTransport {
|
|
2
|
+
baseUrl;
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.baseUrl = config.apiBaseUrl;
|
|
5
|
+
}
|
|
6
|
+
async request(path, init) {
|
|
7
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
8
|
+
const text = await response.text();
|
|
9
|
+
const body = parseJsonOrText(text);
|
|
10
|
+
return {
|
|
11
|
+
status: response.status,
|
|
12
|
+
statusText: response.statusText,
|
|
13
|
+
ok: response.ok,
|
|
14
|
+
body,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function parseJsonOrText(text) {
|
|
19
|
+
if (!text) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(text);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# N2N Post2Site 架构说明(v0.1.2)
|
|
2
|
+
|
|
3
|
+
本文档说明 `n2n-post2site` 当前架构,目标是约束边界、保留接口兼容,并让后续改动有稳定接入点。
|
|
4
|
+
|
|
5
|
+
## 1. 系统定位
|
|
6
|
+
|
|
7
|
+
`n2n-post2site` 是 MCP server,作用是:
|
|
8
|
+
|
|
9
|
+
- 解析环境变量并创建 MCP Server 实例。
|
|
10
|
+
- 向 MCP 客户端暴露 9 个内容发布工具。
|
|
11
|
+
- 将工具参数转为对后端内容 API 的 HTTP 调用。
|
|
12
|
+
- 将后端返回值统一封装为 MCP 可展示文本。
|
|
13
|
+
|
|
14
|
+
它本身不承载持久化、权限管理、审核策略或内容排序决策;这些都应由后端 API 提供方实现。
|
|
15
|
+
|
|
16
|
+
## 2. 分层结构
|
|
17
|
+
|
|
18
|
+
### 2.1 运行入口层(`src/index.ts`)
|
|
19
|
+
|
|
20
|
+
- 使用 `createServer(loadConfig())` 创建 MCP 实例。
|
|
21
|
+
- 使用 `StdioServerTransport` 与 MCP 客户端通信。
|
|
22
|
+
- 仅负责进程生命周期与失败输出。
|
|
23
|
+
|
|
24
|
+
### 2.2 配置层(`src/config.ts`)
|
|
25
|
+
|
|
26
|
+
- 读取环境变量:
|
|
27
|
+
- `CONTENT_API_BASE_URL`
|
|
28
|
+
- `CONTENT_API_KEY`
|
|
29
|
+
- 校验缺失字段并报错。
|
|
30
|
+
- 统一清理 `CONTENT_API_BASE_URL` 末尾斜杠。
|
|
31
|
+
|
|
32
|
+
### 2.3 组装层(`src/server.ts`)
|
|
33
|
+
|
|
34
|
+
- 只做“构建与注册”职责:
|
|
35
|
+
1. 构造 `ContentClient`
|
|
36
|
+
2. 新建 `McpServer`
|
|
37
|
+
3. 注册 9 个工具
|
|
38
|
+
|
|
39
|
+
当前注册顺序:
|
|
40
|
+
|
|
41
|
+
- `n2n_get_capabilities`
|
|
42
|
+
- `n2n_list_posts`
|
|
43
|
+
- `n2n_list_drafts`
|
|
44
|
+
- `n2n_get_post`
|
|
45
|
+
- `n2n_get_product_context`
|
|
46
|
+
- `n2n_create_post`
|
|
47
|
+
- `n2n_update_post`
|
|
48
|
+
- `n2n_update_draft`
|
|
49
|
+
- `n2n_publish_post`
|
|
50
|
+
|
|
51
|
+
### 2.4 工具层(`src/tools/*.ts`)
|
|
52
|
+
|
|
53
|
+
每个文件对应一个 MCP tool:
|
|
54
|
+
|
|
55
|
+
- 参数校验(Zod schema + 业务约束断言)
|
|
56
|
+
- 调用 `ContentClient` 对应方法
|
|
57
|
+
- 返回统一 `text` 结果
|
|
58
|
+
|
|
59
|
+
工具层职责不包含底层 HTTP、请求头、JSON 解析或路由拼接。
|
|
60
|
+
|
|
61
|
+
### 2.5 传输与客户端层
|
|
62
|
+
|
|
63
|
+
- `src/transport/http.ts`
|
|
64
|
+
- `HttpTransport` 接口
|
|
65
|
+
- 默认 `FetchHttpTransport` 封装 `fetch`
|
|
66
|
+
- 负责响应解析(JSON / text)、状态透传
|
|
67
|
+
|
|
68
|
+
- `src/content-client.ts`
|
|
69
|
+
- 统一拼装路径与 HTTP 方法
|
|
70
|
+
- 注入鉴权头(`X-API-KEY` / `Authorization`)
|
|
71
|
+
- 统一处理非 2xx 报错为异常
|
|
72
|
+
- 聚合列表参数与路由参数
|
|
73
|
+
- 实现 `updateDraft` 的业务校验流程:
|
|
74
|
+
1. 先调用 `GET /posts/{id_or_slug}`
|
|
75
|
+
2. 校验 `status === 'draft'`
|
|
76
|
+
3. 才允许 `PATCH /posts/{id_or_slug}`
|
|
77
|
+
|
|
78
|
+
### 2.6 模型与约束层(`src/schemas/blog-post.ts`)
|
|
79
|
+
|
|
80
|
+
- Zod schema 定义所有工具输入约束。
|
|
81
|
+
- `assertContentPostShape` 强化 `type/content_scope` 规则:
|
|
82
|
+
- `type=guide` 必须带 `content_scope`
|
|
83
|
+
- 非 guide 不允许带 `content_scope`
|
|
84
|
+
- `content_scope` 格式必须是 `kind:key`
|
|
85
|
+
|
|
86
|
+
### 2.7 输出层(`src/result.ts`)
|
|
87
|
+
|
|
88
|
+
- 用 `createTextResult` 将任意后端返回内容转为 MCP `content: [{type:'text', text:...}]`。
|
|
89
|
+
- 维持了所有工具一致的返回格式。
|
|
90
|
+
|
|
91
|
+
## 3. 请求流
|
|
92
|
+
|
|
93
|
+
典型调用流(`n2n_create_post`):
|
|
94
|
+
|
|
95
|
+
1. MCP 工具接收参数
|
|
96
|
+
2. Zod schema 校验 + 业务断言
|
|
97
|
+
3. 调用 `ContentClient.createPost`
|
|
98
|
+
4. `ContentClient` 构造请求并通过 `HttpTransport.request` 发起 HTTP
|
|
99
|
+
5. 解析响应体并返回
|
|
100
|
+
6. 工具层用 `createTextResult` 包装并返回给 MCP
|
|
101
|
+
|
|
102
|
+
## 4. 关键不变式
|
|
103
|
+
|
|
104
|
+
- 工具契约不变(9 个工具 + 名称 + 入参语义)
|
|
105
|
+
- 后端接口契约不变(见 `docs/BACKEND_API.md`)
|
|
106
|
+
- CLI/部署接口不变(`bin` 指向 `dist/index.js`)
|
|
107
|
+
- 错误行为保持“尽早失败”:缺少配置或非法参数立即报错
|
|
108
|
+
|
|
109
|
+
## 5. 测试边界
|
|
110
|
+
|
|
111
|
+
- `tests/server.test.ts`:保证 9 个工具完整注册。
|
|
112
|
+
- `tests/transport.test.ts`:验证 `FetchHttpTransport` 和 `ContentClient` 的请求层行为。
|
|
113
|
+
|
|
114
|
+
## 6. 当前架构限制与改造方向
|
|
115
|
+
|
|
116
|
+
当前设计有清晰分层,但仍是“轻量单体” MCP server:
|
|
117
|
+
|
|
118
|
+
- 无运行时重试、幂等保护、限流和退避策略
|
|
119
|
+
- 无完整错误分类体系(目前统一抛错)
|
|
120
|
+
- 工具能力说明仍以静态文档描述为主
|
|
121
|
+
|
|
122
|
+
后续可扩展点:
|
|
123
|
+
|
|
124
|
+
- 新增 `errors.ts` 统一 API 错误模型
|
|
125
|
+
- 在客户端层引入策略性重试/幂等
|
|
126
|
+
- 引入服务层(如 `PostService`)汇聚更复杂业务流
|
|
127
|
+
- 添加端到端契约测试覆盖错误返回形态
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Backend API Contract
|
|
2
|
+
|
|
3
|
+
This document defines the HTTP contract that a backend must implement to work with N2N Post2Site. The MCP server connects to this contract through `CONTENT_API_BASE_URL` and `CONTENT_API_KEY`.
|
|
4
|
+
|
|
5
|
+
## Required endpoints
|
|
6
|
+
|
|
7
|
+
All endpoints are relative to `CONTENT_API_BASE_URL`.
|
|
8
|
+
|
|
9
|
+
```http
|
|
10
|
+
GET /capabilities
|
|
11
|
+
GET /products/{content_scope}
|
|
12
|
+
GET /posts
|
|
13
|
+
POST /posts
|
|
14
|
+
GET /posts/{id_or_slug}
|
|
15
|
+
PATCH /posts/{id_or_slug}
|
|
16
|
+
POST /posts/{id_or_slug}/publish
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## `GET /capabilities`
|
|
20
|
+
|
|
21
|
+
Returns the publishing contract for AI clients, including:
|
|
22
|
+
|
|
23
|
+
- Supported content types.
|
|
24
|
+
- Supported statuses.
|
|
25
|
+
- Supported locales.
|
|
26
|
+
- Single-locale input fields.
|
|
27
|
+
- `content_scope` rules.
|
|
28
|
+
- Available product guide scopes.
|
|
29
|
+
- Safety boundaries.
|
|
30
|
+
|
|
31
|
+
The MCP server calls this endpoint before every create or update operation.
|
|
32
|
+
|
|
33
|
+
## `GET /products/{content_scope}`
|
|
34
|
+
|
|
35
|
+
Returns controlled product context before drafting product guides.
|
|
36
|
+
|
|
37
|
+
Expected response fields:
|
|
38
|
+
|
|
39
|
+
| Field | Description |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `content_scope` | Confirms the valid product guide scope. |
|
|
42
|
+
| `canonical_url` | Product page for deeper reading, links, and citations. |
|
|
43
|
+
| `docs_url` | Docs or guide index to prefer for tutorials. |
|
|
44
|
+
| `summary` | Controlled product summary. |
|
|
45
|
+
| `key_points` | Controlled facts the assistant may rely on. |
|
|
46
|
+
| `do_not_claim` | Claims the assistant must not make. |
|
|
47
|
+
|
|
48
|
+
## `GET /posts`
|
|
49
|
+
|
|
50
|
+
Returns a list of existing posts. Supports filtering by `status`, `type`, `content_scope`, and a search query `q`.
|
|
51
|
+
|
|
52
|
+
For automated internal linking and search, each post in the list should include:
|
|
53
|
+
|
|
54
|
+
| Field | Description |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `id` | Unique identifier. |
|
|
57
|
+
| `title` | Post title. |
|
|
58
|
+
| `slug` | Unique URL slug. |
|
|
59
|
+
| `link` | Required for AI auto-linking. Absolute public URL of the published post on the live website, so AI clients do not guess paths. |
|
|
60
|
+
|
|
61
|
+
## `GET /posts/{id_or_slug}`
|
|
62
|
+
|
|
63
|
+
Returns a single post by ID or slug. The MCP server calls this before updating a post or completing missing locales.
|
|
64
|
+
|
|
65
|
+
## `POST /posts` and `PATCH /posts/{id_or_slug}`
|
|
66
|
+
|
|
67
|
+
Create and update calls use one locale per request.
|
|
68
|
+
|
|
69
|
+
### Payload
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"slug": "example-product-guide",
|
|
74
|
+
"type": "guide",
|
|
75
|
+
"content_scope": "product:example-product",
|
|
76
|
+
"locale": "en",
|
|
77
|
+
"title": "How to Use Example Product",
|
|
78
|
+
"excerpt": "A short guide to using the example product.",
|
|
79
|
+
"content": "## Overview\n\nMarkdown content..."
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Field rules
|
|
84
|
+
|
|
85
|
+
- `title` is plain text.
|
|
86
|
+
- `excerpt` is plain text.
|
|
87
|
+
- `content` is Markdown. Inline HTML is allowed when useful. Full HTML documents with `<html>`, `<head>`, or `<body>` are not allowed.
|
|
88
|
+
- Create and update payloads must not accept `status`, `published_at`, `user_id`, or `author`.
|
|
89
|
+
- Publishing state changes only through `POST /posts/{id_or_slug}/publish`.
|
|
90
|
+
|
|
91
|
+
### Response conventions
|
|
92
|
+
|
|
93
|
+
Backends may return `missing_locales` and `next_actions` after create, update, or publish. The MCP server will add missing language versions with additional update calls instead of asking the backend to auto-translate.
|
|
94
|
+
|
|
95
|
+
## `POST /posts/{id_or_slug}/publish`
|
|
96
|
+
|
|
97
|
+
Publishes an existing draft. Publishing state is managed exclusively through this endpoint, not through create or update payloads.
|
|
98
|
+
|
|
99
|
+
## Draft-specific MCP tools
|
|
100
|
+
|
|
101
|
+
`n2n_list_drafts` and `n2n_update_draft` do not require extra backend endpoints:
|
|
102
|
+
|
|
103
|
+
- `n2n_list_drafts` calls `GET /posts` with `status=draft`.
|
|
104
|
+
- `n2n_update_draft` calls `GET /posts/{id_or_slug}` first and refuses to continue unless the returned post has `status: "draft"`, then calls `PATCH /posts/{id_or_slug}`.
|
|
105
|
+
|
|
106
|
+
## Security requirements
|
|
107
|
+
|
|
108
|
+
- Use a site-scoped API key with the minimum permissions needed.
|
|
109
|
+
- Sanitize backend errors before returning them to the MCP client.
|
|
110
|
+
- Do not expose `user_id`, `author`, `published_at`, or other server-managed fields in create or update payloads.
|
|
111
|
+
- Keep path mapping and field mapping in the backend adapter, not in MCP client configuration.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# N2N Post2Site 重构过程文档(v0.1.x 结构化解耦改造)
|
|
2
|
+
|
|
3
|
+
> 目标:保持对外行为不变,先把模块拆分和职责边界补齐,为后续特性(如动态能力适配)做底座,降低耦合、提高可测试性与可维护性。
|
|
4
|
+
|
|
5
|
+
## 1. 背景与动机
|
|
6
|
+
|
|
7
|
+
当前实现中,`src/index.ts` 承担了以下过多职责:
|
|
8
|
+
|
|
9
|
+
- MCP 工具注册
|
|
10
|
+
- 参数校验与业务流编排
|
|
11
|
+
- HTTP 客户端调用
|
|
12
|
+
- 响应封装
|
|
13
|
+
|
|
14
|
+
`src/content-client.ts` 中也同时承担了请求构建、鉴权、错误处理与状态校验。现有规模虽小,但未来增加能力动态化、重试、错误分类时会迅速耦合。
|
|
15
|
+
|
|
16
|
+
本次改造目标是把耦合点拆开,不新增或变更外部 API 语义。
|
|
17
|
+
|
|
18
|
+
## 2. 改造原则(硬约束)
|
|
19
|
+
|
|
20
|
+
1. 不改变 MCP 工具的调用输入/输出行为。
|
|
21
|
+
2. 不改变后端 Content API 的调用契约。
|
|
22
|
+
3. 不改变命令行入口、环境变量和发布产物协议。
|
|
23
|
+
4. 每个新模块应单一职责,文件改动最小且可回滚。
|
|
24
|
+
5. 保留现有测试覆盖语义;先保证现有测试通过(`npm run check`)。
|
|
25
|
+
|
|
26
|
+
## 3. 现状结构(重构前)
|
|
27
|
+
|
|
28
|
+
- `src/index.ts`
|
|
29
|
+
- 启动 MCP 服务器
|
|
30
|
+
- 注册全部 9 个工具
|
|
31
|
+
- 工具内联处理 request/parse/result 输出
|
|
32
|
+
- `src/content-client.ts`
|
|
33
|
+
- 直接构建并发送 `fetch`
|
|
34
|
+
- 鉴权头、错误处理、JSON 解析集中在同一类
|
|
35
|
+
- 状态校验(`updateDraft`)也在此处理
|
|
36
|
+
- `src/schemas/blog-post.ts`
|
|
37
|
+
- 参数 schema 与运行时断言集中定义
|
|
38
|
+
- `src/tools/`
|
|
39
|
+
- 每个 MCP 工具已拆到独立文件:`get-capabilities.ts`, `list-posts.ts`, `list-drafts.ts`, `get-post.ts`, `get-product-context.ts`, `create-post.ts`, `update-post.ts`, `update-draft.ts`, `publish-post.ts`
|
|
40
|
+
|
|
41
|
+
## 4. 目标结构(重构后)
|
|
42
|
+
|
|
43
|
+
目标是引入以下边界:
|
|
44
|
+
|
|
45
|
+
- `src/server.ts`:仅负责 MCP Server 的实例化与工具注册(`index.ts` 作为入口)。
|
|
46
|
+
- `src/tools/*Tool.ts`:每个 MCP 工具单文件(`get-capabilities.ts`, `list-posts.ts`, `list-drafts.ts`, `get-post.ts`, `get-product-context.ts`, `create-post.ts`, `update-post.ts`, `update-draft.ts`, `publish-post.ts`)。
|
|
47
|
+
- `src/services/`
|
|
48
|
+
- `posts.ts`(可选):封装 `updateDraft` 这类业务流(“读当前状态→校验→更新”)。
|
|
49
|
+
- 后续扩展点:发布、列表拼装、策略判断。
|
|
50
|
+
- `src/transport/http.ts`
|
|
51
|
+
- `HttpTransport` 抽象与默认实现(`fetch` 封装)。
|
|
52
|
+
- 统一注入 `X-API-KEY`、`Authorization`、`Content-Type`。
|
|
53
|
+
- `src/result.ts`
|
|
54
|
+
- 统一 MCP 文本结果格式。
|
|
55
|
+
- `src/errors.ts`(可选)
|
|
56
|
+
- 自定义 API 错误对象(携带 endpoint、method、status、body)。
|
|
57
|
+
- `src/types/`
|
|
58
|
+
- 放置会被服务层复用的公共类型(可选)。
|
|
59
|
+
|
|
60
|
+
> 说明:`src/tools` 目录已存在,可直接使用,先按最小重构只做当前 9 个工具文件即可。
|
|
61
|
+
|
|
62
|
+
## 5. 具体实施步骤
|
|
63
|
+
|
|
64
|
+
### 阶段 1:执行层切分(最低风险)
|
|
65
|
+
|
|
66
|
+
1. 新建 `src/result.ts`,抽离 `textResult()`。
|
|
67
|
+
2. 新建 `src/transport/http.ts` 与 `HttpTransport` 接口。
|
|
68
|
+
3. 重构 `ContentClient` 使用 transport 注入,去掉直接 `fetch` 依赖。
|
|
69
|
+
4. 拆出 `src/server.ts`,让 `index.ts` 只做启动与异常出口。
|
|
70
|
+
|
|
71
|
+
验收标准:
|
|
72
|
+
|
|
73
|
+
- 行为不变(工具响应 JSON 字符串包装不变)。
|
|
74
|
+
- `npm run check` 通过。
|
|
75
|
+
|
|
76
|
+
### 阶段 2:工具拆文件(中等改动)
|
|
77
|
+
|
|
78
|
+
1. 为每个 MCP tool 在 `src/tools/` 下新建处理模块。
|
|
79
|
+
2. 每个模块只做:参数校验 + 对应 service/client 调用 + 返回统一 result。
|
|
80
|
+
3. 将重复文案字符串逐步提取到常量(非强制)。
|
|
81
|
+
|
|
82
|
+
验收标准:
|
|
83
|
+
|
|
84
|
+
- `n2n_get_capabilities` 到 `n2n_publish_post` 全部可从新模块注册。
|
|
85
|
+
- 与现有功能的可见行为一致。
|
|
86
|
+
|
|
87
|
+
### 阶段 3:服务层与业务流集中化(可选但推荐)
|
|
88
|
+
|
|
89
|
+
1. 将 `updateDraft` 中的“先拉取状态再 PATCH”的状态策略移动到 `posts.ts` 服务。
|
|
90
|
+
2. 为未来新增业务动作预留 `PostService`。
|
|
91
|
+
|
|
92
|
+
验收标准:
|
|
93
|
+
|
|
94
|
+
- `updateDraft` 流程不变(仍拒绝非 draft)
|
|
95
|
+
- `tests/client.test.ts` 中的行为断言可保持通过。
|
|
96
|
+
|
|
97
|
+
### 阶段 4:文档与追踪更新
|
|
98
|
+
|
|
99
|
+
1. 在本文件补充阶段完成记录。
|
|
100
|
+
2. 可选地更新 `ROADMAP.md`,若该改造需要对外公开发布说明。
|
|
101
|
+
3. 更新 `CHANGELOG.md`(若版本号/可见改动需要记录)。
|
|
102
|
+
4. 如有新增文件夹,更新仓库文档索引(README 中可选)。
|
|
103
|
+
|
|
104
|
+
> 当前执行状态:阶段 1~2 已完成;阶段 3 作为可选项保留;阶段 4 文档补充已完成(结构变更说明已更新,本次未更新 `ROADMAP.md`/`CHANGELOG.md`)。
|
|
105
|
+
|
|
106
|
+
验收标准:
|
|
107
|
+
|
|
108
|
+
- 代码和文档结构一致。
|
|
109
|
+
- 未引入新外部行为依赖。
|
|
110
|
+
|
|
111
|
+
## 6. 回滚策略
|
|
112
|
+
|
|
113
|
+
- 所有改造分批提交(每个阶段单独 commit),遇到风险可回退最近提交。
|
|
114
|
+
- 每次提交前保留 `main` 分支上的行为对照:通过 `npm run check` 作为门槛。
|
|
115
|
+
- 不在功能层更改任何 schema/接口时,回滚风险低。
|
|
116
|
+
|
|
117
|
+
## 7. 风险与对策
|
|
118
|
+
|
|
119
|
+
1. **工具描述文字重复**
|
|
120
|
+
- 风险:文本变更导致行为认知差异
|
|
121
|
+
- 对策:保持文本不变;仅抽象常量。
|
|
122
|
+
2. **测试脆弱性上升**
|
|
123
|
+
- 风险:拆分后导入路径变化导致 mock 失败
|
|
124
|
+
- 对策:保留现有 public 行为与导入边界,测试逐项执行通过。
|
|
125
|
+
3. **过度重构(scope creep)**
|
|
126
|
+
- 风险:把未要求的能力(动态能力发现)一起引入
|
|
127
|
+
- 对策:本次只做解耦,不新增 schema 或协议。
|
|
128
|
+
|
|
129
|
+
## 8. 交付定义(Acceptance)
|
|
130
|
+
|
|
131
|
+
- `npm run check` 通过。
|
|
132
|
+
- 工具行为回归对照未变:
|
|
133
|
+
- 发布流程
|
|
134
|
+
- 草稿状态检查
|
|
135
|
+
- 产品上下文读取
|
|
136
|
+
- 不引入新的运行时依赖。
|
|
137
|
+
- 不修改后端契约字段。
|
|
138
|
+
|
|
139
|
+
## 9. 里程碑时间建议(可选)
|
|
140
|
+
|
|
141
|
+
- 第一天:阶段 1~2(核心结构搭建)
|
|
142
|
+
- 第一天:阶段 3~4(文档与清理)
|
|
143
|
+
- 第二天:`npm run check` 与 smoke 验证(手工 1 次端到端)
|
|
144
|
+
|
|
145
|
+
## 10. 变更边界声明
|
|
146
|
+
|
|
147
|
+
本次方案不覆盖以下内容:
|
|
148
|
+
|
|
149
|
+
- 动态能力驱动 schema(这是 `ROADMAP` 中 v0.2 规划项)。
|
|
150
|
+
- 发布 API 新增功能。
|
|
151
|
+
- 新增工具。
|
|
152
|
+
- 后端能力模型扩展。
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# n2n-post2site tools reference
|
|
2
|
+
|
|
3
|
+
This document describes the MCP tools provided by n2n-post2site. All tools connect to the backend at `CONTENT_API_BASE_URL` and authenticate with `CONTENT_API_KEY`.
|
|
4
|
+
|
|
5
|
+
See [Backend API Contract](./BACKEND_API.md) for the endpoint specification the backend must implement.
|
|
6
|
+
|
|
7
|
+
## `n2n_get_capabilities`
|
|
8
|
+
|
|
9
|
+
Read backend capabilities before creating or updating content.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The backend returns supported content types, statuses, locales, input fields, `content_scope` rules, and safety boundaries. The MCP server calls this endpoint before every create or update operation.
|
|
16
|
+
|
|
17
|
+
## `n2n_list_posts`
|
|
18
|
+
|
|
19
|
+
Search existing posts before drafting new content.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"status": "draft",
|
|
24
|
+
"type": "guide",
|
|
25
|
+
"content_scope": "product:example-product",
|
|
26
|
+
"q": "setup guide",
|
|
27
|
+
"per_page": 20
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`status` is a filter only. Do not send `status` in create or update calls.
|
|
32
|
+
|
|
33
|
+
## `n2n_list_drafts`
|
|
34
|
+
|
|
35
|
+
List unpublished drafts. This is a draft-only convenience tool over `GET /posts?status=draft`.
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"type": "guide",
|
|
40
|
+
"content_scope": "product:example-product",
|
|
41
|
+
"q": "setup guide",
|
|
42
|
+
"per_page": 20
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `n2n_get_post`
|
|
47
|
+
|
|
48
|
+
Read an existing post before updating it, completing missing locales, or writing a follow-up.
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"id_or_slug": "example-product-guide"
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## `n2n_get_product_context`
|
|
57
|
+
|
|
58
|
+
Read controlled product facts before drafting a product guide.
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"content_scope": "product:example-product"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The backend returns: `content_scope`, `canonical_url`, `docs_url`, `summary`, `key_points`, and `do_not_claim`.
|
|
67
|
+
|
|
68
|
+
## `n2n_create_post`
|
|
69
|
+
|
|
70
|
+
Create a draft. Publishing is a separate step.
|
|
71
|
+
|
|
72
|
+
Company blog example:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"slug": "content-workflow-notes",
|
|
77
|
+
"type": "technical",
|
|
78
|
+
"locale": "en",
|
|
79
|
+
"title": "Content Workflow Notes",
|
|
80
|
+
"excerpt": "Practical notes from shipping a content workflow.",
|
|
81
|
+
"content": "## Notes\n\nMarkdown content..."
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Product guide example:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"slug": "example-product-guide",
|
|
90
|
+
"type": "guide",
|
|
91
|
+
"content_scope": "product:example-product",
|
|
92
|
+
"locale": "en",
|
|
93
|
+
"title": "How to Use Example Product",
|
|
94
|
+
"excerpt": "A short guide to using the example product.",
|
|
95
|
+
"content": "## Overview\n\nMarkdown content..."
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## `n2n_update_post`
|
|
100
|
+
|
|
101
|
+
Update one locale of an existing post. Call `n2n_get_post` first.
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"id_or_slug": "example-product-guide",
|
|
106
|
+
"locale": "de",
|
|
107
|
+
"title": "So verwenden Sie Example Product",
|
|
108
|
+
"excerpt": "A localized summary for the selected locale.",
|
|
109
|
+
"content": "## Ueberblick\n\nLocalized Markdown content..."
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## `n2n_update_draft`
|
|
114
|
+
|
|
115
|
+
Update one locale of an unpublished draft. The client reads the post first and refuses to patch unless the backend reports `status: "draft"`.
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"id_or_slug": "example-product-guide",
|
|
120
|
+
"locale": "en",
|
|
121
|
+
"title": "How to Use Example Product",
|
|
122
|
+
"excerpt": "A refined summary for the draft.",
|
|
123
|
+
"content": "## Overview\n\nUpdated draft Markdown content..."
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## `n2n_publish_post`
|
|
128
|
+
|
|
129
|
+
Publish an existing draft.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"id_or_slug": "example-product-guide"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Publishing state is managed exclusively through this tool, not through create or update payloads.
|
|
138
|
+
|
|
139
|
+
## Client method mapping
|
|
140
|
+
|
|
141
|
+
- `ContentClient.listDrafts(input)` calls `GET /posts` with `status=draft` plus the provided list filters.
|
|
142
|
+
- `ContentClient.updateDraft(input)` calls `GET /posts/{id_or_slug}` first, checks `status === "draft"`, then calls `PATCH /posts/{id_or_slug}`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n2n-post2site",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "AI-assisted content publishing MCP server for blogs and product guides.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,9 +31,16 @@
|
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
|
+
"assets",
|
|
35
|
+
"docs",
|
|
34
36
|
"README.md",
|
|
35
37
|
"CHANGELOG.md",
|
|
36
38
|
".env.example"
|
|
37
39
|
],
|
|
40
|
+
"author": "N2NS Lab",
|
|
41
|
+
"funding": {
|
|
42
|
+
"type": "github",
|
|
43
|
+
"url": "https://github.com/sponsors/n2ns"
|
|
44
|
+
},
|
|
38
45
|
"license": "MIT"
|
|
39
46
|
}
|