n2n-post2site 0.1.3 → 0.1.4

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 CHANGED
@@ -4,9 +4,19 @@ All notable changes to N2N Post2Site are documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
- ### Added
7
+ ## [0.1.4] - 2026-06-24
8
+
9
+ ### Changed
10
+
11
+ - **Breaking:** renamed the `n2n_get_product_context` tool to `n2n_get_scope_context`, and the backend endpoint from `GET /products/{content_scope}` to `GET /scopes/{content_scope}`.
12
+ - Treat `content_scope` as generic `kind:key` metadata. Client-side validation now only checks the `kind:key` format; whether a scope is required or prohibited for a content type is decided by the backend (`capabilities.content.content_scope.required_for_types`).
13
+ - Relaxed `type` and `locale` tool inputs from fixed enums to free strings; the backend's `capabilities` is the source of truth for supported values. `status` remains an enum.
14
+ - Genericized tool descriptions and documentation to remove product-only assumptions.
15
+
16
+ ### Removed
8
17
 
9
- - None yet.
18
+ - Removed the client-side `type`/`content_scope` coupling (`assertContentPostShape`), replaced by a format-only `assertContentScopeFormat`.
19
+ - Removed the internal `docs/REFACTOR_PLAN.md` working document.
10
20
 
11
21
  ## [0.1.3] - 2026-06-17
12
22
 
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Datafrog LLC
3
+ Copyright (c) 2026 N2NS Lab
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -4,14 +4,13 @@
4
4
 
5
5
  # n2n-post2site
6
6
 
7
- AI-assisted content publishing MCP server for blogs and product guides.
7
+ AI-assisted content publishing MCP server for blogs, guides, and other categorized content.
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/n2n-post2site)](https://www.npmjs.com/package/n2n-post2site)
10
10
  [![npm total downloads](https://img.shields.io/npm/dt/n2n-post2site)](https://www.npmjs.com/package/n2n-post2site)
11
- [![license](https://img.shields.io/github/license/n2ns/n2n-post2site)](https://github.com/n2ns/n2n-post2site/blob/main/LICENSE)
11
+ [![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
12
12
  [![MCP Protocol](https://img.shields.io/badge/MCP-Protocol-blue)](https://modelcontextprotocol.io)
13
13
  [![node version](https://img.shields.io/node/v/n2n-post2site)](https://nodejs.org)
14
- [![DataFrog.io](https://datafrog.io/badges/datafrog.svg)](https://datafrog.io)
15
14
 
16
15
  ---
17
16
 
@@ -19,28 +18,70 @@ AI-assisted content publishing MCP server for blogs and product guides.
19
18
 
20
19
  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.
21
20
 
21
+ ## 📚 Contents
22
+
23
+ - [What is n2n-post2site?](#-what-is-n2n-post2site)
24
+ - [Architecture](#️-architecture)
25
+ - [Publishing model](#-publishing-model)
26
+ - [Quick start](#-quick-start)
27
+ - [Backend API contract](#-backend-api-contract)
28
+ - [MCP tools](#️-mcp-tools)
29
+ - [Content format](#-content-format)
30
+ - [Security and governance notes](#-security-and-governance-notes)
31
+ - [Related docs](#-related-docs)
32
+
22
33
  ## 💡 What is n2n-post2site?
23
34
 
24
35
  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.
25
36
 
26
37
  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.
27
38
 
39
+ ## 🏗️ Architecture
40
+
41
+ ```text
42
+ ┌─────────────────────────────┐
43
+ │ AI coding assistant │
44
+ │ (Claude, Cursor, VS Code) │
45
+ └──────────────┬──────────────┘
46
+ │ MCP tool calls (stdio)
47
+
48
+ ┌─────────────────────────────┐
49
+ │ n2n-post2site │
50
+ │ MCP server · 9 tools │
51
+ │ validate → map → HTTP call │
52
+ └──────────────┬──────────────┘
53
+ │ HTTPS + site-scoped API key
54
+
55
+ ┌─────────────────────────────┐
56
+ │ Your backend content API │
57
+ │ (Content Publishing API │
58
+ │ Contract) │
59
+ │ owns storage, auth, policy │
60
+ └─────────────────────────────┘
61
+ ```
62
+
63
+ - **The MCP server is a thin bridge**: it validates tool input, maps it to the contract, and forwards HTTP calls. It owns no persistence, authorization, or review policy.
64
+ - **The backend owns the truth**: storage, publishing state, and access control all live behind the contract.
65
+ - **One server, one site**: each MCP instance is bound to a single website through `CONTENT_API_BASE_URL` and `CONTENT_API_KEY`.
66
+
67
+ See [Architecture](./docs/ARCHITECTURE.md) for the full layer breakdown.
68
+
28
69
  ## 📰 Publishing model
29
70
 
30
- n2n-post2site works with two publishing spaces:
71
+ n2n-post2site classifies content with an optional `content_scope`:
31
72
 
32
- | Space | `content_scope` | Example |
73
+ | Content | `content_scope` | Example |
33
74
  |---|---|---|
34
- | Company blog | omitted or empty | technical notes, announcements, changelogs |
35
- | Product guide | `kind:key` | `product:evisa-helper` |
75
+ | Unscoped | omitted or empty | technical notes, announcements, changelogs |
76
+ | Scoped | `kind:key` | `product:example-product` |
36
77
 
37
- 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`.
78
+ The backend defines which `content_scope` kinds are valid and which content types require one. Scoped content should only be written after the assistant reads controlled context with `n2n_get_scope_context`.
38
79
 
39
80
  The assistant should follow this workflow:
40
81
 
41
82
  1. Call `n2n_get_capabilities`.
42
83
  2. Search existing content with `n2n_list_posts`.
43
- 3. For product guides, call `n2n_get_product_context`.
84
+ 3. For scoped content, call `n2n_get_scope_context`.
44
85
  4. Create or update one locale at a time.
45
86
  5. Resume unfinished work with `n2n_list_drafts`, `n2n_get_post`, and `n2n_update_draft`.
46
87
  6. Review the draft.
@@ -50,6 +91,8 @@ The assistant should follow this workflow:
50
91
 
51
92
  **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).
52
93
 
94
+ > **Just trying it out?** Run the [mock backend](./examples/mock-backend/) for a zero-dependency local server that implements the full contract — no website required.
95
+
53
96
  ### 1. Configure your MCP client
54
97
 
55
98
  Using the npm package:
@@ -99,9 +142,9 @@ See **[Backend API Contract](./docs/BACKEND_API.md)** for the full specification
99
142
 
100
143
  ## 🛠️ MCP tools
101
144
 
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.
145
+ - **Discovery** (`n2n_get_capabilities`, `n2n_list_posts`, `n2n_get_scope_context`): read backend capabilities, list existing posts, and load scope context before drafting.
146
+ - **Drafting** (`n2n_create_post`, `n2n_update_post`, `n2n_update_draft`, `n2n_list_drafts`, `n2n_get_post`): create posts, update posts one locale at a time, and resume unpublished drafts.
147
+ - **Publishing** (`n2n_publish_post`): publish an approved draft through an explicit publish step.
105
148
 
106
149
  See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call examples.
107
150
 
@@ -159,4 +202,4 @@ This project is licensed under the [MIT License](./LICENSE).
159
202
 
160
203
  ---
161
204
 
162
- Built by [N2NS Lab](https://n2ns.com), the open-source lab of [datafrog.io](https://datafrog.io) for practical AI developer tools.
205
+ Built by [N2NS Lab](https://n2ns.com), an open-source lab for practical AI developer tools.
@@ -1,7 +1,7 @@
1
1
  import { FetchHttpTransport } from './transport/http.js';
2
2
  const POSTS_PATH = '/posts';
3
3
  const CAPABILITIES_PATH = '/capabilities';
4
- const PRODUCTS_PATH = '/products';
4
+ const SCOPES_PATH = '/scopes';
5
5
  export class ContentClient {
6
6
  config;
7
7
  transport;
@@ -29,8 +29,8 @@ export class ContentClient {
29
29
  async getPost(idOrSlug) {
30
30
  return this.request(`${POSTS_PATH}/${encodeURIComponent(idOrSlug)}`, { method: 'GET' });
31
31
  }
32
- async getProductContext(input) {
33
- return this.request(`${PRODUCTS_PATH}/${encodeURIComponent(input.content_scope)}`, { method: 'GET' });
32
+ async getScopeContext(input) {
33
+ return this.request(`${SCOPES_PATH}/${encodeURIComponent(input.content_scope)}`, { method: 'GET' });
34
34
  }
35
35
  async createPost(input) {
36
36
  return this.request(POSTS_PATH, {
@@ -1,27 +1,26 @@
1
1
  import { z } from 'zod';
2
- export const contentTypeSchema = z.enum(['technical', 'announcement', 'changelog', 'guide']);
3
2
  export const statusSchema = z.enum(['draft', 'published']);
4
- export const localeSchema = z.enum(['en', 'zh_CN', 'es', 'de']);
5
3
  export const capabilitiesSchema = z.object({});
4
+ const CONTENT_SCOPE_FORMAT = /^[a-z][a-z0-9_-]*:[a-z0-9][a-z0-9_-]*$/;
6
5
  export const listPostsSchema = z.object({
7
6
  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.'),
7
+ type: z.string().optional().describe('Filter by content type. Call n2n_get_capabilities for supported types.'),
8
+ content_scope: z.string().optional().describe('Filter by content_scope (kind:key). Use an empty string to list unscoped posts when the backend supports it.'),
9
+ q: z.string().optional().describe('Search query across title and content.'),
11
10
  per_page: z.number().int().min(1).max(100).optional().describe('Page size. Defaults to the server value.'),
12
11
  });
13
12
  export const listDraftsSchema = listPostsSchema.omit({ status: true });
14
13
  export const getPostSchema = z.object({
15
14
  id_or_slug: z.string().min(1).describe('Numeric ID or slug of the article.'),
16
15
  });
17
- export const productContextSchema = z.object({
18
- 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.'),
16
+ export const scopeContextSchema = z.object({
17
+ content_scope: z.string().min(1).describe('Content scope in kind:key format, for example product:example. Read this before writing scoped content. Call n2n_get_capabilities for supported kinds.'),
19
18
  });
20
19
  export const createPostSchema = z.object({
21
20
  slug: z.string().min(1).describe('Globally unique URL slug.'),
22
- type: contentTypeSchema.default('technical').describe('Use guide for product or collection guide articles.'),
23
- 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.'),
24
- locale: localeSchema.default('en').describe('Locale for this single-language submission. Submit one locale per tool call.'),
21
+ type: z.string().optional().describe('Content type. Call n2n_get_capabilities for supported types. The backend applies its default when omitted.'),
22
+ content_scope: z.string().optional().describe('Optional kind:key categorization, for example product:example. The backend requires it for certain content types; call n2n_get_capabilities for supported kinds, examples, and which types require it.'),
23
+ locale: z.string().optional().describe('Single-locale code for this submission. Submit one locale per call. Call n2n_get_capabilities for supported locales. The backend applies its default when omitted.'),
25
24
  title: z.string().min(1).describe('Plain text title for the selected locale. Do not use Markdown here.'),
26
25
  excerpt: z.string().optional().describe('Plain text summary for the selected locale. No Markdown headings, tables, or images.'),
27
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.'),
@@ -32,16 +31,13 @@ export const updatePostSchema = createPostSchema.partial().extend({
32
31
  });
33
32
  export const updateDraftSchema = updatePostSchema;
34
33
  export const publishPostSchema = getPostSchema;
35
- export function assertContentPostShape(input) {
36
- const type = input.type ?? 'technical';
37
- const content_scope = input.content_scope;
38
- if (type === 'guide' && !content_scope) {
39
- 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.');
40
- }
41
- if (type !== 'guide' && content_scope) {
42
- throw new Error('content_scope is only allowed when type is guide. Omit content_scope for unscoped company blog posts.');
43
- }
44
- if (content_scope && !/^[a-z][a-z0-9_-]*:[a-z0-9][a-z0-9_-]*$/.test(content_scope)) {
45
- throw new Error('content_scope must use kind:key format, for example product:example-product.');
34
+ /**
35
+ * Contract-level format check only. Whether a content_scope is required or
36
+ * prohibited for a given type is decided by the backend (capabilities
37
+ * .content.content_scope.required_for_types) and enforced server-side.
38
+ */
39
+ export function assertContentScopeFormat(contentScope) {
40
+ if (contentScope && !CONTENT_SCOPE_FORMAT.test(contentScope)) {
41
+ throw new Error('content_scope must use kind:key format, for example product:example.');
46
42
  }
47
43
  }
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@ import { ContentClient } from './content-client.js';
3
3
  import { registerCreatePostTool } from './tools/create-post.js';
4
4
  import { registerGetCapabilitiesTool } from './tools/get-capabilities.js';
5
5
  import { registerGetPostTool } from './tools/get-post.js';
6
- import { registerGetProductContextTool } from './tools/get-product-context.js';
6
+ import { registerGetScopeContextTool } from './tools/get-scope-context.js';
7
7
  import { registerListDraftsTool } from './tools/list-drafts.js';
8
8
  import { registerListPostsTool } from './tools/list-posts.js';
9
9
  import { registerPublishPostTool } from './tools/publish-post.js';
@@ -19,7 +19,7 @@ export function createServer(config) {
19
19
  registerListPostsTool(server, client);
20
20
  registerListDraftsTool(server, client);
21
21
  registerGetPostTool(server, client);
22
- registerGetProductContextTool(server, client);
22
+ registerGetScopeContextTool(server, client);
23
23
  registerCreatePostTool(server, client);
24
24
  registerUpdatePostTool(server, client);
25
25
  registerUpdateDraftTool(server, client);
@@ -1,9 +1,9 @@
1
1
  import { createTextResult } from '../result.js';
2
- import { assertContentPostShape, createPostSchema } from '../schemas/blog-post.js';
2
+ import { assertContentScopeFormat, createPostSchema } from '../schemas/blog-post.js';
3
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) => {
4
+ server.tool('n2n_create_post', 'Create a content draft with one locale per call. Before creating new content, search existing posts with n2n_list_posts to avoid duplicates. Call n2n_get_capabilities to learn supported types and which require a content_scope; for scoped content also call n2n_get_scope_context and follow the returned facts. 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
5
  const parsed = createPostSchema.parse(input);
6
- assertContentPostShape(parsed);
6
+ assertContentScopeFormat(parsed.content_scope);
7
7
  const result = await client.createPost(parsed);
8
8
  return createTextResult(result);
9
9
  });
@@ -1,7 +1,7 @@
1
1
  import { createTextResult } from '../result.js';
2
2
  import { capabilitiesSchema } from '../schemas/blog-post.js';
3
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) => {
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 (kinds, examples, which types require a scope), and available scopes.', capabilitiesSchema.shape, async (input) => {
5
5
  capabilitiesSchema.parse(input);
6
6
  const result = await client.getCapabilities();
7
7
  return createTextResult(result);
@@ -0,0 +1,10 @@
1
+ import { createTextResult } from '../result.js';
2
+ import { assertContentScopeFormat, scopeContextSchema } from '../schemas/blog-post.js';
3
+ export function registerGetScopeContextTool(server, client) {
4
+ server.tool('n2n_get_scope_context', 'Read the controlled context for a content_scope before writing scoped content. The backend returns content_scope plus host-defined facts (for example canonical_url, docs_url, summary, key_points, do_not_claim) so the article does not invent facts.', scopeContextSchema.shape, async (input) => {
5
+ const parsed = scopeContextSchema.parse(input);
6
+ assertContentScopeFormat(parsed.content_scope);
7
+ const result = await client.getScopeContext(parsed);
8
+ return createTextResult(result);
9
+ });
10
+ }
@@ -1,7 +1,7 @@
1
1
  import { createTextResult } from '../result.js';
2
2
  import { listDraftsSchema } from '../schemas/blog-post.js';
3
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) => {
4
+ server.tool('n2n_list_drafts', 'List unpublished draft posts. 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
5
  const parsed = listDraftsSchema.parse(input);
6
6
  const result = await client.listDrafts(parsed);
7
7
  return createTextResult(result);
@@ -1,7 +1,7 @@
1
1
  import { createTextResult } from '../result.js';
2
2
  import { listPostsSchema } from '../schemas/blog-post.js';
3
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) => {
4
+ server.tool('n2n_list_posts', 'Search and list existing posts before drafting new content, to avoid duplicate topics and conflicting guidance. Filter by content_scope for scoped content, or content_scope="" for unscoped posts when the backend supports it.', listPostsSchema.shape, async (input) => {
5
5
  const parsed = listPostsSchema.parse(input);
6
6
  const result = await client.listPosts(parsed);
7
7
  return createTextResult(result);
@@ -1,9 +1,9 @@
1
1
  import { createTextResult } from '../result.js';
2
- import { assertContentPostShape, updateDraftSchema } from '../schemas/blog-post.js';
2
+ import { assertContentScopeFormat, updateDraftSchema } from '../schemas/blog-post.js';
3
3
  export function registerUpdateDraftTool(server, client) {
4
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
5
  const parsed = updateDraftSchema.parse(input);
6
- assertContentPostShape(parsed);
6
+ assertContentScopeFormat(parsed.content_scope);
7
7
  const result = await client.updateDraft(parsed);
8
8
  return createTextResult(result);
9
9
  });
@@ -1,9 +1,9 @@
1
1
  import { createTextResult } from '../result.js';
2
- import { assertContentPostShape, updatePostSchema } from '../schemas/blog-post.js';
2
+ import { assertContentScopeFormat, updatePostSchema } from '../schemas/blog-post.js';
3
3
  export function registerUpdatePostTool(server, client) {
4
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
5
  const parsed = updatePostSchema.parse(input);
6
- assertContentPostShape(parsed);
6
+ assertContentScopeFormat(parsed.content_scope);
7
7
  const result = await client.updatePost(parsed);
8
8
  return createTextResult(result);
9
9
  });
@@ -1,127 +1,113 @@
1
- # N2N Post2Site 架构说明(v0.1.2)
1
+ # N2N Post2Site Architecture
2
2
 
3
- 本文档说明 `n2n-post2site` 当前架构,目标是约束边界、保留接口兼容,并让后续改动有稳定接入点。
3
+ This document describes the current architecture of `n2n-post2site`. The goal is to keep boundaries clear, preserve interface compatibility, and give future changes stable extension points.
4
4
 
5
- ## 1. 系统定位
5
+ ## 1. What it is
6
6
 
7
- `n2n-post2site` MCP server,作用是:
7
+ `n2n-post2site` is an MCP server. It:
8
8
 
9
- - 解析环境变量并创建 MCP Server 实例。
10
- - MCP 客户端暴露 9 个内容发布工具。
11
- - 将工具参数转为对后端内容 API HTTP 调用。
12
- - 将后端返回值统一封装为 MCP 可展示文本。
9
+ - Loads environment configuration and creates an MCP server instance.
10
+ - Exposes nine content publishing tools to MCP clients.
11
+ - Translates tool arguments into HTTP calls against a backend content API.
12
+ - Wraps backend responses into MCP-displayable text.
13
13
 
14
- 它本身不承载持久化、权限管理、审核策略或内容排序决策;这些都应由后端 API 提供方实现。
14
+ It does not own persistence, authorization, review policy, or content-ranking decisions. Those belong to the backend API provider.
15
15
 
16
- ## 2. 分层结构
16
+ ## 2. Layers
17
17
 
18
- ### 2.1 运行入口层(`src/index.ts`)
18
+ ### 2.1 Entry (`src/index.ts`)
19
19
 
20
- - 使用 `createServer(loadConfig())` 创建 MCP 实例。
21
- - 使用 `StdioServerTransport` MCP 客户端通信。
22
- - 仅负责进程生命周期与失败输出。
20
+ - Creates the server via `createServer(loadConfig())`.
21
+ - Connects to the MCP client over `StdioServerTransport`.
22
+ - Owns only process lifecycle and failure output.
23
23
 
24
- ### 2.2 配置层(`src/config.ts`)
24
+ ### 2.2 Configuration (`src/config.ts`)
25
25
 
26
- - 读取环境变量:
27
- - `CONTENT_API_BASE_URL`
28
- - `CONTENT_API_KEY`
29
- - 校验缺失字段并报错。
30
- - 统一清理 `CONTENT_API_BASE_URL` 末尾斜杠。
26
+ - Reads `CONTENT_API_BASE_URL` and `CONTENT_API_KEY`.
27
+ - Fails fast on missing values.
28
+ - Normalizes trailing slashes on `CONTENT_API_BASE_URL`.
31
29
 
32
- ### 2.3 组装层(`src/server.ts`)
30
+ ### 2.3 Assembly (`src/server.ts`)
33
31
 
34
- - 只做“构建与注册”职责:
35
- 1. 构造 `ContentClient`
36
- 2. 新建 `McpServer`
37
- 3. 注册 9 个工具
32
+ Build-and-register only:
38
33
 
39
- 当前注册顺序:
34
+ 1. Construct `ContentClient`.
35
+ 2. Create `McpServer`.
36
+ 3. Register the nine tools, in order:
40
37
 
41
38
  - `n2n_get_capabilities`
42
39
  - `n2n_list_posts`
43
40
  - `n2n_list_drafts`
44
41
  - `n2n_get_post`
45
- - `n2n_get_product_context`
42
+ - `n2n_get_scope_context`
46
43
  - `n2n_create_post`
47
44
  - `n2n_update_post`
48
45
  - `n2n_update_draft`
49
46
  - `n2n_publish_post`
50
47
 
51
- ### 2.4 工具层(`src/tools/*.ts`)
48
+ ### 2.4 Tools (`src/tools/*.ts`)
52
49
 
53
- 每个文件对应一个 MCP tool
50
+ Each file is one MCP tool:
54
51
 
55
- - 参数校验(Zod schema + 业务约束断言)
56
- - 调用 `ContentClient` 对应方法
57
- - 返回统一 `text` 结果
52
+ - Validate input (Zod schema + lightweight assertions).
53
+ - Call the matching `ContentClient` method.
54
+ - Return a uniform `text` result.
58
55
 
59
- 工具层职责不包含底层 HTTP、请求头、JSON 解析或路由拼接。
56
+ Tools do not deal with low-level HTTP, headers, JSON parsing, or path building.
60
57
 
61
- ### 2.5 传输与客户端层
58
+ ### 2.5 Transport and client
62
59
 
63
- - `src/transport/http.ts`
64
- - `HttpTransport` 接口
65
- - 默认 `FetchHttpTransport` 封装 `fetch`
66
- - 负责响应解析(JSON / text)、状态透传
60
+ - `src/transport/http.ts` — the `HttpTransport` interface and the default `FetchHttpTransport` wrapping `fetch`; handles response parsing (JSON/text) and status passthrough.
61
+ - `src/content-client.ts` — builds paths and HTTP methods, injects auth headers (`X-API-KEY` / `Authorization`), turns non-2xx responses into errors, assembles list and route parameters, and implements the `updateDraft` flow:
62
+ 1. `GET /posts/{id_or_slug}`
63
+ 2. require `status === 'draft'`
64
+ 3. then `PATCH /posts/{id_or_slug}`
67
65
 
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}`
66
+ ### 2.6 Schemas and validation (`src/schemas/blog-post.ts`)
77
67
 
78
- ### 2.6 模型与约束层(`src/schemas/blog-post.ts`)
68
+ - Zod schemas define every tool's input.
69
+ - `type` and `locale` are free strings; the backend is the source of truth for supported values (discoverable via `n2n_get_capabilities`).
70
+ - `assertContentScopeFormat` performs a contract-level format check only: when present, `content_scope` must be `kind:key`. Whether a scope is *required* or *prohibited* for a given type is decided and enforced by the backend (`capabilities.content.content_scope.required_for_types`).
79
71
 
80
- - Zod schema 定义所有工具输入约束。
81
- - `assertContentPostShape` 强化 `type/content_scope` 规则:
82
- - `type=guide` 必须带 `content_scope`
83
- - 非 guide 不允许带 `content_scope`
84
- - `content_scope` 格式必须是 `kind:key`
72
+ ### 2.7 Output (`src/result.ts`)
85
73
 
86
- ### 2.7 输出层(`src/result.ts`)
74
+ - `createTextResult` wraps any backend payload into MCP `content: [{ type: 'text', text: ... }]`, keeping all tools' return shape consistent.
87
75
 
88
- - `createTextResult` 将任意后端返回内容转为 MCP `content: [{type:'text', text:...}]`。
89
- - 维持了所有工具一致的返回格式。
76
+ ## 3. Request flow
90
77
 
91
- ## 3. 请求流
78
+ Typical call (`n2n_create_post`):
92
79
 
93
- 典型调用流(`n2n_create_post`):
80
+ 1. The tool receives arguments.
81
+ 2. Zod schema validation + assertions.
82
+ 3. `ContentClient.createPost` is called.
83
+ 4. `ContentClient` builds the request and sends it via `HttpTransport.request`.
84
+ 5. The response body is parsed and returned.
85
+ 6. The tool wraps it with `createTextResult` for the MCP client.
94
86
 
95
- 1. MCP 工具接收参数
96
- 2. Zod schema 校验 + 业务断言
97
- 3. 调用 `ContentClient.createPost`
98
- 4. `ContentClient` 构造请求并通过 `HttpTransport.request` 发起 HTTP
99
- 5. 解析响应体并返回
100
- 6. 工具层用 `createTextResult` 包装并返回给 MCP
87
+ ## 4. Key invariants
101
88
 
102
- ## 4. 关键不变式
89
+ - Tool contract is stable (nine tools, names, argument semantics).
90
+ - Backend contract is stable (see `docs/BACKEND_API.md`).
91
+ - CLI/deploy entry is stable (`bin` points to `dist/index.js`).
92
+ - Fail-fast error behavior: missing config or invalid arguments error immediately.
103
93
 
104
- - 工具契约不变(9 个工具 + 名称 + 入参语义)
105
- - 后端接口契约不变(见 `docs/BACKEND_API.md`)
106
- - CLI/部署接口不变(`bin` 指向 `dist/index.js`)
107
- - 错误行为保持“尽早失败”:缺少配置或非法参数立即报错
94
+ ## 5. Test boundaries
108
95
 
109
- ## 5. 测试边界
96
+ - `tests/server.test.ts` — all nine tools register.
97
+ - `tests/transport.test.ts` / `tests/client.test.ts` — request-layer behavior of `FetchHttpTransport` and `ContentClient`.
98
+ - `tests/schema.test.ts` — input schemas and `content_scope` format checks.
110
99
 
111
- - `tests/server.test.ts`:保证 9 个工具完整注册。
112
- - `tests/transport.test.ts`:验证 `FetchHttpTransport` 和 `ContentClient` 的请求层行为。
100
+ ## 6. Limits and future direction
113
101
 
114
- ## 6. 当前架构限制与改造方向
102
+ The design is layered but intentionally a lightweight monolith:
115
103
 
116
- 当前设计有清晰分层,但仍是“轻量单体” MCP server:
104
+ - No runtime retry, idempotency, rate limiting, or backoff.
105
+ - A single error mode (errors are thrown uniformly).
106
+ - Tool capabilities are documented statically.
117
107
 
118
- - 无运行时重试、幂等保护、限流和退避策略
119
- - 无完整错误分类体系(目前统一抛错)
120
- - 工具能力说明仍以静态文档描述为主
108
+ Possible extensions:
121
109
 
122
- 后续可扩展点:
123
-
124
- - 新增 `errors.ts` 统一 API 错误模型
125
- - 在客户端层引入策略性重试/幂等
126
- - 引入服务层(如 `PostService`)汇聚更复杂业务流
127
- - 添加端到端契约测试覆盖错误返回形态
110
+ - An `errors.ts` with a unified API error model.
111
+ - Strategic retry / idempotency in the client layer.
112
+ - A service layer (e.g. `PostService`) for richer flows.
113
+ - End-to-end contract tests covering error response shapes.
@@ -8,7 +8,7 @@ All endpoints are relative to `CONTENT_API_BASE_URL`.
8
8
 
9
9
  ```http
10
10
  GET /capabilities
11
- GET /products/{content_scope}
11
+ GET /scopes/{content_scope}
12
12
  GET /posts
13
13
  POST /posts
14
14
  GET /posts/{id_or_slug}
@@ -24,24 +24,24 @@ Returns the publishing contract for AI clients, including:
24
24
  - Supported statuses.
25
25
  - Supported locales.
26
26
  - Single-locale input fields.
27
- - `content_scope` rules.
28
- - Available product guide scopes.
27
+ - `content_scope` rules (`content.content_scope`: format, `kinds`, `examples`, `required_for_types`).
28
+ - Available scopes (`scopes`).
29
29
  - Safety boundaries.
30
30
 
31
31
  The MCP server calls this endpoint before every create or update operation.
32
32
 
33
- ## `GET /products/{content_scope}`
33
+ ## `GET /scopes/{content_scope}`
34
34
 
35
- Returns controlled product context before drafting product guides.
35
+ Returns controlled context for a `content_scope` before drafting scoped content.
36
36
 
37
- Expected response fields:
37
+ The body is `content_scope` plus host-defined controlled fields. Common fields:
38
38
 
39
39
  | Field | Description |
40
40
  | --- | --- |
41
- | `content_scope` | Confirms the valid product guide scope. |
42
- | `canonical_url` | Product page for deeper reading, links, and citations. |
41
+ | `content_scope` | Confirms the valid scope. |
42
+ | `canonical_url` | Page for deeper reading, links, and citations. |
43
43
  | `docs_url` | Docs or guide index to prefer for tutorials. |
44
- | `summary` | Controlled product summary. |
44
+ | `summary` | Controlled summary. |
45
45
  | `key_points` | Controlled facts the assistant may rely on. |
46
46
  | `do_not_claim` | Claims the assistant must not make. |
47
47
 
@@ -85,6 +85,7 @@ Create and update calls use one locale per request.
85
85
  - `title` is plain text.
86
86
  - `excerpt` is plain text.
87
87
  - `content` is Markdown. Inline HTML is allowed when useful. Full HTML documents with `<html>`, `<head>`, or `<body>` are not allowed.
88
+ - `content_scope` is an optional `kind:key` value. The backend requires it for the content types listed in `capabilities.content.content_scope.required_for_types` and prohibits it for all other types.
88
89
  - Create and update payloads must not accept `status`, `published_at`, `user_id`, or `author`.
89
90
  - Publishing state changes only through `POST /posts/{id_or_slug}/publish`.
90
91
 
@@ -53,9 +53,9 @@ Read an existing post before updating it, completing missing locales, or writing
53
53
  }
54
54
  ```
55
55
 
56
- ## `n2n_get_product_context`
56
+ ## `n2n_get_scope_context`
57
57
 
58
- Read controlled product facts before drafting a product guide.
58
+ Read controlled facts for a `content_scope` before drafting scoped content.
59
59
 
60
60
  ```json
61
61
  {
@@ -63,13 +63,13 @@ Read controlled product facts before drafting a product guide.
63
63
  }
64
64
  ```
65
65
 
66
- The backend returns: `content_scope`, `canonical_url`, `docs_url`, `summary`, `key_points`, and `do_not_claim`.
66
+ The backend returns `content_scope` plus host-defined controlled fields, commonly `canonical_url`, `docs_url`, `summary`, `key_points`, and `do_not_claim`.
67
67
 
68
68
  ## `n2n_create_post`
69
69
 
70
70
  Create a draft. Publishing is a separate step.
71
71
 
72
- Company blog example:
72
+ Blog post example:
73
73
 
74
74
  ```json
75
75
  {
@@ -82,7 +82,7 @@ Company blog example:
82
82
  }
83
83
  ```
84
84
 
85
- Product guide example:
85
+ Scoped content example (a type that requires a content_scope):
86
86
 
87
87
  ```json
88
88
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2n-post2site",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "AI-assisted content publishing MCP server for blogs and product guides.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,10 +0,0 @@
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
- }
@@ -1,152 +0,0 @@
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
- - 后端能力模型扩展。