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 +12 -2
- package/LICENSE +1 -1
- package/README.md +56 -13
- package/dist/content-client.js +3 -3
- package/dist/schemas/blog-post.js +17 -21
- package/dist/server.js +2 -2
- package/dist/tools/create-post.js +3 -3
- package/dist/tools/get-capabilities.js +1 -1
- package/dist/tools/get-scope-context.js +10 -0
- package/dist/tools/list-drafts.js +1 -1
- package/dist/tools/list-posts.js +1 -1
- package/dist/tools/update-draft.js +2 -2
- package/dist/tools/update-post.js +2 -2
- package/docs/ARCHITECTURE.md +69 -83
- package/docs/BACKEND_API.md +10 -9
- package/docs/TOOLS_REFERENCE.md +5 -5
- package/package.json +1 -1
- package/dist/tools/get-product-context.js +0 -10
- package/docs/REFACTOR_PLAN.md +0 -152
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
|
-
|
|
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
|
-
-
|
|
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
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
|
|
7
|
+
AI-assisted content publishing MCP server for blogs, guides, and other categorized content.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/n2n-post2site)
|
|
10
10
|
[](https://www.npmjs.com/package/n2n-post2site)
|
|
11
|
-
[](./LICENSE)
|
|
12
12
|
[](https://modelcontextprotocol.io)
|
|
13
13
|
[](https://nodejs.org)
|
|
14
|
-
[](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
|
|
71
|
+
n2n-post2site classifies content with an optional `content_scope`:
|
|
31
72
|
|
|
32
|
-
|
|
|
73
|
+
| Content | `content_scope` | Example |
|
|
33
74
|
|---|---|---|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
75
|
+
| Unscoped | omitted or empty | technical notes, announcements, changelogs |
|
|
76
|
+
| Scoped | `kind:key` | `product:example-product` |
|
|
36
77
|
|
|
37
|
-
The backend defines which
|
|
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
|
|
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
|
|
103
|
-
- **Drafting
|
|
104
|
-
- **Publishing
|
|
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),
|
|
205
|
+
Built by [N2NS Lab](https://n2ns.com), an open-source lab for practical AI developer tools.
|
package/dist/content-client.js
CHANGED
|
@@ -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
|
|
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
|
|
33
|
-
return this.request(`${
|
|
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:
|
|
9
|
-
content_scope: z.string().optional().describe('
|
|
10
|
-
q: z.string().optional().describe('Search query across title
|
|
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
|
|
18
|
-
content_scope: z.string().min(1).describe('
|
|
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:
|
|
23
|
-
content_scope: z.string().optional().describe('
|
|
24
|
-
locale:
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
throw new Error('content_scope
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
2
|
+
import { assertContentScopeFormat, createPostSchema } from '../schemas/blog-post.js';
|
|
3
3
|
export function registerCreatePostTool(server, client) {
|
|
4
|
-
server.tool('n2n_create_post', 'Create
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
package/dist/tools/list-posts.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
6
|
+
assertContentScopeFormat(parsed.content_scope);
|
|
7
7
|
const result = await client.updatePost(parsed);
|
|
8
8
|
return createTextResult(result);
|
|
9
9
|
});
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,127 +1,113 @@
|
|
|
1
|
-
# N2N Post2Site
|
|
1
|
+
# N2N Post2Site Architecture
|
|
2
2
|
|
|
3
|
-
|
|
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`
|
|
7
|
+
`n2n-post2site` is an MCP server. It:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
|
18
|
+
### 2.1 Entry (`src/index.ts`)
|
|
19
19
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
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
|
|
24
|
+
### 2.2 Configuration (`src/config.ts`)
|
|
25
25
|
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
- `
|
|
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
|
|
48
|
+
### 2.4 Tools (`src/tools/*.ts`)
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
Each file is one MCP tool:
|
|
54
51
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
52
|
+
- Validate input (Zod schema + lightweight assertions).
|
|
53
|
+
- Call the matching `ContentClient` method.
|
|
54
|
+
- Return a uniform `text` result.
|
|
58
55
|
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
- `createTextResult` wraps any backend payload into MCP `content: [{ type: 'text', text: ... }]`, keeping all tools' return shape consistent.
|
|
87
75
|
|
|
88
|
-
|
|
89
|
-
- 维持了所有工具一致的返回格式。
|
|
76
|
+
## 3. Request flow
|
|
90
77
|
|
|
91
|
-
|
|
78
|
+
Typical call (`n2n_create_post`):
|
|
92
79
|
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
- 后端接口契约不变(见 `docs/BACKEND_API.md`)
|
|
106
|
-
- CLI/部署接口不变(`bin` 指向 `dist/index.js`)
|
|
107
|
-
- 错误行为保持“尽早失败”:缺少配置或非法参数立即报错
|
|
94
|
+
## 5. Test boundaries
|
|
108
95
|
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
- `tests/transport.test.ts`:验证 `FetchHttpTransport` 和 `ContentClient` 的请求层行为。
|
|
100
|
+
## 6. Limits and future direction
|
|
113
101
|
|
|
114
|
-
|
|
102
|
+
The design is layered but intentionally a lightweight monolith:
|
|
115
103
|
|
|
116
|
-
|
|
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
|
-
-
|
|
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.
|
package/docs/BACKEND_API.md
CHANGED
|
@@ -8,7 +8,7 @@ All endpoints are relative to `CONTENT_API_BASE_URL`.
|
|
|
8
8
|
|
|
9
9
|
```http
|
|
10
10
|
GET /capabilities
|
|
11
|
-
GET /
|
|
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
|
|
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 /
|
|
33
|
+
## `GET /scopes/{content_scope}`
|
|
34
34
|
|
|
35
|
-
Returns controlled
|
|
35
|
+
Returns controlled context for a `content_scope` before drafting scoped content.
|
|
36
36
|
|
|
37
|
-
|
|
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
|
|
42
|
-
| `canonical_url` |
|
|
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
|
|
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
|
|
package/docs/TOOLS_REFERENCE.md
CHANGED
|
@@ -53,9 +53,9 @@ Read an existing post before updating it, completing missing locales, or writing
|
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
## `
|
|
56
|
+
## `n2n_get_scope_context`
|
|
57
57
|
|
|
58
|
-
Read controlled
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
Scoped content example (a type that requires a content_scope):
|
|
86
86
|
|
|
87
87
|
```json
|
|
88
88
|
{
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|
package/docs/REFACTOR_PLAN.md
DELETED
|
@@ -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
|
-
- 后端能力模型扩展。
|