n2n-post2site 0.1.2
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/.env.example +2 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +352 -0
- package/dist/config.js +14 -0
- package/dist/content-client.js +77 -0
- package/dist/index.js +68 -0
- package/dist/schemas/blog-post.js +45 -0
- package/package.json +39 -0
package/.env.example
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to N2N Post2Site are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.2] - 2026-06-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `n2n_get_product_context` for reading a controlled product fact sheet before drafting product guides.
|
|
10
|
+
- Documented the required product context fields: `content_scope`, `canonical_url`, `docs_url`, `summary`, `key_points`, and `do_not_claim`.
|
|
11
|
+
- Added client and schema coverage for product context lookups.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Product guide creation guidance now requires reading product context before drafting.
|
|
16
|
+
- Content creation guidance now requires searching existing posts first, and update guidance requires reading the target post first.
|
|
17
|
+
- Clarified that create/update must not send publication governance fields such as `status`, `published_at`, `user_id`, or `author`; publishing remains a separate explicit tool call.
|
|
18
|
+
- Version bumped to `0.1.2`.
|
|
19
|
+
|
|
20
|
+
## [0.1.1] - 2026-06-15
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Switched create and update inputs to a single-locale model with flat `locale`, `title`, `excerpt`, and `content` fields.
|
|
25
|
+
- Removed create/update exposure for direct publication fields from the MCP schema.
|
|
26
|
+
- Documented `missing_locales` and `next_actions` follow-up flow.
|
|
27
|
+
|
|
28
|
+
## [0.1.0] - 2026-06-14
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Initial local MCP server for AI-assisted content publishing.
|
|
33
|
+
- Tools for capabilities, listing posts, reading posts, creating drafts, updating drafts, and publishing posts.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Datafrog LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# N2N Post2Site
|
|
2
|
+
|
|
3
|
+
AI-assisted content publishing MCP server for blogs and product guides.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/n2n-post2site)
|
|
6
|
+
[](https://www.npmjs.com/package/n2n-post2site)
|
|
7
|
+
[](https://github.com/n2ns/n2n-post2site/blob/main/LICENSE)
|
|
8
|
+
[](https://modelcontextprotocol.io)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://datafrog.io)
|
|
11
|
+
|
|
12
|
+
N2N Post2Site lets an AI assistant draft, edit, review, and publish website content through a narrow Content Publishing API Contract. It is built for teams that want AI-assisted blog posts, technical notes, changelogs, and product guides without giving the assistant database access, shell access, deployment access, payment access, or user administration access.
|
|
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
|
|
17
|
+
|
|
18
|
+
- Runs locally as an MCP server.
|
|
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.
|
|
26
|
+
|
|
27
|
+
## When to use it
|
|
28
|
+
|
|
29
|
+
Use N2N Post2Site when you want an AI assistant to help with:
|
|
30
|
+
|
|
31
|
+
- Company blog posts.
|
|
32
|
+
- Product guides.
|
|
33
|
+
- Technical field notes.
|
|
34
|
+
- Release notes and changelogs.
|
|
35
|
+
- Localized article drafts.
|
|
36
|
+
- Safe updates to existing website content.
|
|
37
|
+
|
|
38
|
+
Do not use it as a CMS. It does not provide an admin panel, database schema, storage backend, preview system, deployment workflow, or image upload service.
|
|
39
|
+
|
|
40
|
+
## Publishing model
|
|
41
|
+
|
|
42
|
+
N2N Post2Site works with two publishing spaces:
|
|
43
|
+
|
|
44
|
+
| Space | `content_scope` | Example |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Company blog | omitted or empty | technical notes, announcements, changelogs |
|
|
47
|
+
| Product guide | `kind:key` | `product:evisa-helper` |
|
|
48
|
+
|
|
49
|
+
The backend defines which scopes are valid. A product guide should only be written after the assistant reads controlled product context with `n2n_get_product_context`.
|
|
50
|
+
|
|
51
|
+
The assistant should follow this workflow:
|
|
52
|
+
|
|
53
|
+
1. Call `n2n_get_capabilities`.
|
|
54
|
+
2. Search existing content with `n2n_list_posts`.
|
|
55
|
+
3. For product guides, call `n2n_get_product_context`.
|
|
56
|
+
4. Create or update one locale at a time.
|
|
57
|
+
5. Review the draft.
|
|
58
|
+
6. Publish only through `n2n_publish_post`.
|
|
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.
|
|
93
|
+
|
|
94
|
+
Keep path mapping and field mapping in the backend adapter, not in MCP client configuration. The MCP config should only need a base URL and an API key.
|
|
95
|
+
|
|
96
|
+
Do not put API keys in prompts, article content, README examples, or screenshots.
|
|
97
|
+
|
|
98
|
+
## MCP client examples
|
|
99
|
+
|
|
100
|
+
### Using the npm package
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"n2n-post2site": {
|
|
106
|
+
"command": "npx",
|
|
107
|
+
"args": ["-y", "n2n-post2site"],
|
|
108
|
+
"env": {
|
|
109
|
+
"CONTENT_API_BASE_URL": "https://example.com/api/v1/mcp",
|
|
110
|
+
"CONTENT_API_KEY": "change-me"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Using a local checkout
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"n2n-post2site": {
|
|
123
|
+
"command": "node",
|
|
124
|
+
"args": ["/path/to/n2n-post2site/dist/index.js"],
|
|
125
|
+
"env": {
|
|
126
|
+
"CONTENT_API_BASE_URL": "https://example.com/api/v1/mcp",
|
|
127
|
+
"CONTENT_API_KEY": "change-me"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Bind one MCP server configuration to exactly one website. Do not ask the AI assistant to choose a `site_id` in tool arguments.
|
|
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:
|
|
192
|
+
|
|
193
|
+
- `title` is plain text.
|
|
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`.
|
|
200
|
+
|
|
201
|
+
Backends may return `missing_locales` and `next_actions` after create, update, or publish. The assistant should add missing language versions with additional `n2n_update_post` calls instead of asking the backend to auto-translate.
|
|
202
|
+
|
|
203
|
+
## MCP tools
|
|
204
|
+
|
|
205
|
+
### `n2n_get_capabilities`
|
|
206
|
+
|
|
207
|
+
Read backend capabilities before creating or updating content.
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `n2n_list_posts`
|
|
214
|
+
|
|
215
|
+
Search existing posts before drafting new content.
|
|
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
|
+
```
|
|
303
|
+
|
|
304
|
+
## Content format
|
|
305
|
+
|
|
306
|
+
- Submit one locale per create or update call.
|
|
307
|
+
- Use Markdown for `content`.
|
|
308
|
+
- Use fenced code blocks for commands, JSON, YAML, and code examples.
|
|
309
|
+
- Use Markdown image syntax for images.
|
|
310
|
+
- Always include descriptive image alt text.
|
|
311
|
+
- Reference public image URLs or existing site paths.
|
|
312
|
+
- This MCP does not upload images in v1.
|
|
313
|
+
|
|
314
|
+
Image example:
|
|
315
|
+
|
|
316
|
+
```md
|
|
317
|
+

|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Safety boundaries
|
|
321
|
+
|
|
322
|
+
N2N Post2Site should not expose:
|
|
323
|
+
|
|
324
|
+
- Delete operations.
|
|
325
|
+
- Product configuration writes.
|
|
326
|
+
- Pricing plan writes.
|
|
327
|
+
- User administration.
|
|
328
|
+
- Payment or subscription administration.
|
|
329
|
+
- Database queries.
|
|
330
|
+
- Log access.
|
|
331
|
+
- Shell commands.
|
|
332
|
+
- Deployment or server operations.
|
|
333
|
+
|
|
334
|
+
Recommended backend behavior:
|
|
335
|
+
|
|
336
|
+
- Sanitize backend errors before returning them to the AI client.
|
|
337
|
+
- Use a site-scoped API key with the minimum permissions needed.
|
|
338
|
+
- Create drafts by default.
|
|
339
|
+
- Keep create/update and publish separate.
|
|
340
|
+
- Set `published_at` on the backend, not from MCP input.
|
|
341
|
+
|
|
342
|
+
## Laravel backend package direction
|
|
343
|
+
|
|
344
|
+
A Laravel package can implement the HTTP side of this contract for multiple Laravel sites without copying controllers between projects. N2N Post2Site should stay a generic MCP client of that contract.
|
|
345
|
+
|
|
346
|
+
## About N2NS Lab
|
|
347
|
+
|
|
348
|
+
Built by N2NS Lab, short for Next-to-Native Systems Lab, Datafrog's open-source lab for AI-native developer tools.
|
|
349
|
+
|
|
350
|
+
Learn more: https://n2ns.com
|
|
351
|
+
|
|
352
|
+
Source repository: git@github.com:n2ns/n2n-post2site.git
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function loadConfig(env = process.env) {
|
|
2
|
+
const apiBaseUrl = normalizeBaseUrl(env.CONTENT_API_BASE_URL ?? '');
|
|
3
|
+
const apiKey = env.CONTENT_API_KEY;
|
|
4
|
+
if (!apiBaseUrl) {
|
|
5
|
+
throw new Error('CONTENT_API_BASE_URL is required. Point it at your protected content API base URL.');
|
|
6
|
+
}
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
throw new Error('CONTENT_API_KEY is required. Pass a site-scoped content API token as an environment variable.');
|
|
9
|
+
}
|
|
10
|
+
return { apiBaseUrl, apiKey };
|
|
11
|
+
}
|
|
12
|
+
function normalizeBaseUrl(value) {
|
|
13
|
+
return value.replace(/\/+$/, '');
|
|
14
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const POSTS_PATH = '/posts';
|
|
2
|
+
const CAPABILITIES_PATH = '/capabilities';
|
|
3
|
+
const PRODUCTS_PATH = '/products';
|
|
4
|
+
export class ContentClient {
|
|
5
|
+
config;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async getCapabilities() {
|
|
10
|
+
return this.request(CAPABILITIES_PATH, { method: 'GET' });
|
|
11
|
+
}
|
|
12
|
+
async listPosts(input) {
|
|
13
|
+
const params = new URLSearchParams();
|
|
14
|
+
for (const [key, value] of Object.entries(input)) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
params.set(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
20
|
+
return this.request(`${POSTS_PATH}${suffix}`, { method: 'GET' });
|
|
21
|
+
}
|
|
22
|
+
async getPost(idOrSlug) {
|
|
23
|
+
return this.request(`${POSTS_PATH}/${encodeURIComponent(idOrSlug)}`, { method: 'GET' });
|
|
24
|
+
}
|
|
25
|
+
async getProductContext(input) {
|
|
26
|
+
return this.request(`${PRODUCTS_PATH}/${encodeURIComponent(input.content_scope)}`, { method: 'GET' });
|
|
27
|
+
}
|
|
28
|
+
async createPost(input) {
|
|
29
|
+
return this.request(POSTS_PATH, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: JSON.stringify(input),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async updatePost(input) {
|
|
35
|
+
const { id_or_slug: idOrSlug, ...payload } = input;
|
|
36
|
+
return this.request(`${POSTS_PATH}/${encodeURIComponent(idOrSlug)}`, {
|
|
37
|
+
method: 'PATCH',
|
|
38
|
+
body: JSON.stringify(payload),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async publishPost(idOrSlug) {
|
|
42
|
+
return this.request(`${POSTS_PATH}/${encodeURIComponent(idOrSlug)}/publish`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async request(path, init) {
|
|
47
|
+
const response = await fetch(`${this.config.apiBaseUrl}${path}`, {
|
|
48
|
+
...init,
|
|
49
|
+
headers: {
|
|
50
|
+
Accept: 'application/json',
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
'X-API-KEY': this.config.apiKey,
|
|
53
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
54
|
+
...(init.headers ?? {}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
const body = parseJsonOrText(text);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Content API request failed: ${response.status} ${response.statusText}: ${formatBody(body)}`);
|
|
61
|
+
}
|
|
62
|
+
return body;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseJsonOrText(text) {
|
|
66
|
+
if (!text)
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function formatBody(body) {
|
|
76
|
+
return typeof body === 'string' ? body : JSON.stringify(body, null, 2);
|
|
77
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { ContentClient } from './content-client.js';
|
|
6
|
+
import { assertContentPostShape, capabilitiesSchema, createPostSchema, getPostSchema, listPostsSchema, productContextSchema, publishPostSchema, updatePostSchema, } from './schemas/blog-post.js';
|
|
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
|
+
}
|
|
60
|
+
async function main() {
|
|
61
|
+
const transport = new StdioServerTransport();
|
|
62
|
+
await server.connect(transport);
|
|
63
|
+
}
|
|
64
|
+
main().catch((error) => {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
console.error(`n2n-post2site failed: ${message}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const contentTypeSchema = z.enum(['technical', 'announcement', 'changelog', 'guide']);
|
|
3
|
+
export const statusSchema = z.enum(['draft', 'published']);
|
|
4
|
+
export const localeSchema = z.enum(['en', 'zh_CN', 'es', 'de']);
|
|
5
|
+
export const capabilitiesSchema = z.object({});
|
|
6
|
+
export const listPostsSchema = z.object({
|
|
7
|
+
status: statusSchema.optional().describe('Filter by publication status.'),
|
|
8
|
+
type: contentTypeSchema.optional().describe('Filter by content type. Use guide for product or collection guide articles.'),
|
|
9
|
+
content_scope: z.string().optional().describe('Optional site-defined canonical publishing scope for guides, products, or collections. Use kind:key, for example product:example-product. Use an empty string to list unscoped company blog posts when the backend supports it.'),
|
|
10
|
+
q: z.string().optional().describe('Search query across title, content, and translations.'),
|
|
11
|
+
per_page: z.number().int().min(1).max(100).optional().describe('Page size. Defaults to the server value.'),
|
|
12
|
+
});
|
|
13
|
+
export const getPostSchema = z.object({
|
|
14
|
+
id_or_slug: z.string().min(1).describe('Numeric ID or slug of the article.'),
|
|
15
|
+
});
|
|
16
|
+
export const productContextSchema = z.object({
|
|
17
|
+
content_scope: z.string().min(1).describe('Product guide scope in kind:key format, for example product:evisa-helper. Read this before writing a product guide.'),
|
|
18
|
+
});
|
|
19
|
+
export const createPostSchema = z.object({
|
|
20
|
+
slug: z.string().min(1).describe('Globally unique URL slug.'),
|
|
21
|
+
type: contentTypeSchema.default('technical').describe('Use guide for product or collection guide articles.'),
|
|
22
|
+
content_scope: z.string().optional().describe('Required when type is guide. Use kind:key, for example product:example-product, project:example-project, or collection:example-collection.'),
|
|
23
|
+
locale: localeSchema.default('en').describe('Locale for this single-language submission. Submit one locale per tool call.'),
|
|
24
|
+
title: z.string().min(1).describe('Plain text title for the selected locale. Do not use Markdown here.'),
|
|
25
|
+
excerpt: z.string().optional().describe('Plain text summary for the selected locale. No Markdown headings, tables, or images.'),
|
|
26
|
+
content: z.string().min(1).describe('Markdown article body for the selected locale. Markdown image syntax is allowed. Inline HTML is allowed when useful.'),
|
|
27
|
+
thumbnail: z.string().optional().describe('Optional public image path or URL for the article thumbnail.'),
|
|
28
|
+
});
|
|
29
|
+
export const updatePostSchema = createPostSchema.partial().extend({
|
|
30
|
+
id_or_slug: z.string().min(1).describe('Numeric ID or slug of the article to update.'),
|
|
31
|
+
});
|
|
32
|
+
export const publishPostSchema = getPostSchema;
|
|
33
|
+
export function assertContentPostShape(input) {
|
|
34
|
+
const type = input.type ?? 'technical';
|
|
35
|
+
const content_scope = input.content_scope;
|
|
36
|
+
if (type === 'guide' && !content_scope) {
|
|
37
|
+
throw new Error('content_scope is required when type is guide. Use a kind:key value such as product:example-product, project:example-project, or collection:example-collection.');
|
|
38
|
+
}
|
|
39
|
+
if (type !== 'guide' && content_scope) {
|
|
40
|
+
throw new Error('content_scope is only allowed when type is guide. Omit content_scope for unscoped company blog posts.');
|
|
41
|
+
}
|
|
42
|
+
if (content_scope && !/^[a-z][a-z0-9_-]*:[a-z0-9][a-z0-9_-]*$/.test(content_scope)) {
|
|
43
|
+
throw new Error('content_scope must use kind:key format, for example product:example-product.');
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n2n-post2site",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "AI-assisted content publishing MCP server for blogs and product guides.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+ssh://git@github.com/n2ns/n2n-post2site.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"n2n-post2site": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rm -rf dist && tsc && chmod 755 dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"check": "npm run build && npm test"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
21
|
+
"zod": "^3.25.76"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.0.0",
|
|
25
|
+
"tsx": "^4.22.4",
|
|
26
|
+
"typescript": "^5.8.3",
|
|
27
|
+
"vitest": "^4.1.8"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
".env.example"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
}
|