cscec-gwdoc-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.5
2
+ Name: cscec-gwdoc-mcp
3
+ Version: 0.1.0
4
+ Summary: 中建企业公文 Word(.docx)生成 stdio MCP 服务,供百炼函数计算 uvx 托管接入。
5
+ Author: CSCEC
6
+ License: Proprietary
7
+ Keywords: cscec,docx,mcp,word,公文
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: lxml>=4.9
10
+ Requires-Dist: mcp[cli]<2,>=1.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: python-docx>=1.1
13
+ Description-Content-Type: text/markdown
14
+
15
+ # cscec-gwdoc-mcp
16
+
17
+ 中建企业公文 Word(.docx)生成 **stdio MCP** 服务。可被 `uvx` 安装运行,适配阿里云百炼函数计算(FC)代码包(uvx)托管模式,再由中建 AI 等外部 MCP 客户端经百炼接入。
18
+
19
+ ## 工具
20
+
21
+ `generate_official_doc`:根据结构化公文内容生成 `.docx`。
22
+
23
+ | 参数 | 类型 | 说明 |
24
+ | --- | --- | --- |
25
+ | `title` | str | 公文标题(必填) |
26
+ | `sections` | list[dict] | 章节列表,每项 `{heading, paragraphs, subheadings?}` |
27
+ | `document_number` | str | 文号 |
28
+ | `recipient` | str | 主送单位 |
29
+ | `introduction` | str | 导语 |
30
+ | `attachments` | list[str] | 附件名称 |
31
+ | `issuer` | str | 发文单位 |
32
+ | `department` | str | 发文部门(代章) |
33
+ | `issue_date` | str | 发文日期 |
34
+ | `cc` | str | 抄送 |
35
+ | `document_type` | str | 公文类型,默认「通报」 |
36
+
37
+ ## 运行
38
+
39
+ ```bash
40
+ uvx cscec-gwdoc-mcp
41
+ ```
42
+
43
+ 服务以 stdio 方式启动;生成的 `.docx` 作为 MCP 内嵌二进制资源(base64)随工具结果回传,不提供公网下载链接。
44
+
45
+ ## 本地开发自测
46
+
47
+ ```bash
48
+ uv venv
49
+ uv pip install -e .
50
+ python -m cscec_gwdoc_mcp.server
51
+ ```
52
+
53
+ ## 数据与排版说明
54
+
55
+ - 生成器优先套用包内范文的页面设置、Word 样式及页眉页脚,再重建正文。
56
+ - 未提供的文号、日期、单位、统计数字、整改时限等应标记为待补充,人工核对后再正式发文。
57
+ - 「方正小标宋简体」「仿宋_GB2312」「楷体_GB2312」等字体需目标环境安装,缺字体会被自动替换。
@@ -0,0 +1,43 @@
1
+ # cscec-gwdoc-mcp
2
+
3
+ 中建企业公文 Word(.docx)生成 **stdio MCP** 服务。可被 `uvx` 安装运行,适配阿里云百炼函数计算(FC)代码包(uvx)托管模式,再由中建 AI 等外部 MCP 客户端经百炼接入。
4
+
5
+ ## 工具
6
+
7
+ `generate_official_doc`:根据结构化公文内容生成 `.docx`。
8
+
9
+ | 参数 | 类型 | 说明 |
10
+ | --- | --- | --- |
11
+ | `title` | str | 公文标题(必填) |
12
+ | `sections` | list[dict] | 章节列表,每项 `{heading, paragraphs, subheadings?}` |
13
+ | `document_number` | str | 文号 |
14
+ | `recipient` | str | 主送单位 |
15
+ | `introduction` | str | 导语 |
16
+ | `attachments` | list[str] | 附件名称 |
17
+ | `issuer` | str | 发文单位 |
18
+ | `department` | str | 发文部门(代章) |
19
+ | `issue_date` | str | 发文日期 |
20
+ | `cc` | str | 抄送 |
21
+ | `document_type` | str | 公文类型,默认「通报」 |
22
+
23
+ ## 运行
24
+
25
+ ```bash
26
+ uvx cscec-gwdoc-mcp
27
+ ```
28
+
29
+ 服务以 stdio 方式启动;生成的 `.docx` 作为 MCP 内嵌二进制资源(base64)随工具结果回传,不提供公网下载链接。
30
+
31
+ ## 本地开发自测
32
+
33
+ ```bash
34
+ uv venv
35
+ uv pip install -e .
36
+ python -m cscec_gwdoc_mcp.server
37
+ ```
38
+
39
+ ## 数据与排版说明
40
+
41
+ - 生成器优先套用包内范文的页面设置、Word 样式及页眉页脚,再重建正文。
42
+ - 未提供的文号、日期、单位、统计数字、整改时限等应标记为待补充,人工核对后再正式发文。
43
+ - 「方正小标宋简体」「仿宋_GB2312」「楷体_GB2312」等字体需目标环境安装,缺字体会被自动替换。
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ """中建企业公文 Word 生成 stdio MCP 服务。"""
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1,166 @@
1
+ # -*- coding: utf-8 -*-
2
+ """stdio MCP 服务:中建企业公文 Word 生成。
3
+
4
+ 通过 FastMCP 以 stdio 方式运行(供百炼函数计算 uvx 托管)。
5
+ 生成的 .docx 不再走公网下载链接,而是作为 MCP 内嵌二进制资源(base64)
6
+ 随工具结果回传。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import re
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+ from urllib.parse import quote
17
+
18
+ from importlib.resources import files
19
+
20
+ from mcp.server.fastmcp import FastMCP
21
+ from mcp.types import BlobResourceContents, EmbeddedResource, TextContent
22
+
23
+ from .service import OfficialDocument, SectionItem, build_document
24
+
25
+ DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
26
+ ASSET_NAME = "公文格式范文.docx"
27
+
28
+ mcp = FastMCP("cscec-gwdoc-mcp")
29
+
30
+
31
+ def _template_bytes() -> bytes:
32
+ """读取包内范文模板二进制。"""
33
+ return files("cscec_gwdoc_mcp.assets").joinpath(ASSET_NAME).read_bytes()
34
+
35
+
36
+ def _safe_filename(title: str) -> str:
37
+ """清理文件名中的非法字符。"""
38
+ name = re.sub(r'[\\/:*?"<>|\r\n]+', "_", title).strip()
39
+ return name or "公文"
40
+
41
+
42
+ def _build_document_payload(
43
+ title: str,
44
+ sections: Optional[List[Dict[str, Any]]] = None,
45
+ document_number: str = "",
46
+ recipient: str = "",
47
+ introduction: str = "",
48
+ attachments: Optional[List[str]] = None,
49
+ issuer: str = "",
50
+ department: str = "",
51
+ issue_date: str = "",
52
+ cc: str = "",
53
+ document_type: str = "通报",
54
+ ) -> OfficialDocument:
55
+ section_items: List[SectionItem] = []
56
+ for raw in sections or []:
57
+ if not isinstance(raw, dict):
58
+ continue
59
+ section_items.append(
60
+ SectionItem(
61
+ heading=str(raw.get("heading", "")),
62
+ paragraphs=[str(p) for p in raw.get("paragraphs", []) or []],
63
+ subheadings=[str(s) for s in raw.get("subheadings", []) or []],
64
+ )
65
+ )
66
+ return OfficialDocument(
67
+ title=title,
68
+ sections=section_items,
69
+ document_number=document_number,
70
+ recipient=recipient,
71
+ introduction=introduction,
72
+ attachments=list(attachments or []),
73
+ issuer=issuer,
74
+ department=department,
75
+ issue_date=issue_date,
76
+ cc=cc,
77
+ document_type=document_type,
78
+ )
79
+
80
+
81
+ def render_docx(payload: OfficialDocument) -> tuple[str, bytes]:
82
+ """在临时目录生成 docx,返回 (文件名, 文件二进制)。"""
83
+ template_bytes = _template_bytes()
84
+ with tempfile.TemporaryDirectory() as tmp:
85
+ out_path = Path(tmp) / f"{_safe_filename(payload.title)}.docx"
86
+ build_document(payload, out_path, template_bytes)
87
+ return out_path.name, out_path.read_bytes()
88
+
89
+
90
+ @mcp.tool()
91
+ def generate_official_doc(
92
+ title: str,
93
+ sections: Optional[List[Dict[str, Any]]] = None,
94
+ document_number: str = "",
95
+ recipient: str = "",
96
+ introduction: str = "",
97
+ attachments: Optional[List[str]] = None,
98
+ issuer: str = "",
99
+ department: str = "",
100
+ issue_date: str = "",
101
+ cc: str = "",
102
+ document_type: str = "通报",
103
+ ) -> List[Any]:
104
+ """根据结构化公文内容生成 Word(.docx)公文。
105
+
106
+ 所有数据必须来自用户资料;未提供的文号、日期、单位、统计数字、
107
+ 整改时限等应在调用前标记为待补充,不要自行编造。
108
+
109
+ :param title: 公文标题(必填)
110
+ :param sections: 正文章节列表,每项含 heading(章节标题)、
111
+ paragraphs(正文段落列表)、subheadings(可选,括号序号子标题列表)
112
+ :param document_number: 文号,如「中建四华建安监函字〔2026〕60号」
113
+ :param recipient: 主送单位,如「司属各项目:」
114
+ :param introduction: 导语 / 开头说明
115
+ :param attachments: 附件名称列表
116
+ :param issuer: 发文单位
117
+ :param department: 发文部门(代章)
118
+ :param issue_date: 发文日期
119
+ :param cc: 抄送范围
120
+ :param document_type: 公文类型,默认「通报」
121
+ :return: 生成的 .docx 文件(内嵌二进制资源)及文字说明
122
+ """
123
+ payload = _build_document_payload(
124
+ title=title,
125
+ sections=sections,
126
+ document_number=document_number,
127
+ recipient=recipient,
128
+ introduction=introduction,
129
+ attachments=attachments,
130
+ issuer=issuer,
131
+ department=department,
132
+ issue_date=issue_date,
133
+ cc=cc,
134
+ document_type=document_type,
135
+ )
136
+
137
+ filename, data = render_docx(payload)
138
+ blob = base64.b64encode(data).decode()
139
+
140
+ text = (
141
+ f"公文已生成:{filename}\n"
142
+ f"类型:{payload.document_type};标题:{payload.title}\n"
143
+ f"文件大小:{len(data)} 字节;格式:.docx(Word)。\n"
144
+ f"正文共 {len(payload.sections)} 个章节;未提供的文号、日期、单位、"
145
+ f"数据与时限等需人工核对后再正式发文。"
146
+ )
147
+
148
+ return [
149
+ TextContent(type="text", text=text),
150
+ EmbeddedResource(
151
+ type="resource",
152
+ resource=BlobResourceContents(
153
+ uri=f"file:///{quote(filename)}",
154
+ mimeType=DOCX_MIME,
155
+ blob=blob,
156
+ ),
157
+ ),
158
+ ]
159
+
160
+
161
+ def main() -> None:
162
+ mcp.run(transport="stdio")
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main()
@@ -0,0 +1,244 @@
1
+ # -*- coding: utf-8 -*-
2
+ """公文构建核心逻辑(纯函数,不含任何 HTTP / 服务层)。
3
+
4
+ 由原 word_service.py 的 FastAPI 版本剥离而来:保留 OfficialDocument /
5
+ SectionItem 模型与 build_document 排版逻辑;模板范文从包内资源读取,
6
+ 产物写入调用方给定的输出路径。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import io
12
+ import re
13
+ from pathlib import Path
14
+ from typing import List, Optional
15
+
16
+ from docx import Document
17
+ from docx.document import Document as DocumentType
18
+ from docx.oxml import OxmlElement
19
+ from docx.oxml.ns import qn
20
+ from docx.shared import Pt
21
+ from docx.text.paragraph import Paragraph
22
+ from lxml import etree
23
+ from pydantic import BaseModel, Field
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # 数据模型
28
+ # ---------------------------------------------------------------------------
29
+
30
+ class SectionItem(BaseModel):
31
+ """正文章节。
32
+
33
+ heading:章节标题;paragraphs:正文段落列表;
34
+ subheadings:子标题(括号序号标题)列表,顺序排在正文段落之前。
35
+ """
36
+
37
+ heading: str
38
+ paragraphs: List[str] = Field(default_factory=list)
39
+ subheadings: List[str] = Field(default_factory=list)
40
+
41
+
42
+ class OfficialDocument(BaseModel):
43
+ """结构化公文内容。所有未提供的字段应在提示词层标记为待补充。"""
44
+
45
+ title: str
46
+ sections: List[SectionItem] = Field(default_factory=list)
47
+ document_number: str = ""
48
+ recipient: str = ""
49
+ introduction: str = ""
50
+ attachments: List[str] = Field(default_factory=list)
51
+ issuer: str = ""
52
+ department: str = ""
53
+ issue_date: str = ""
54
+ cc: str = ""
55
+ document_type: str = "通报"
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # 字号 / 字体工具
60
+ # ---------------------------------------------------------------------------
61
+
62
+ def _pt(size: Optional[str]) -> Optional[Pt]:
63
+ """将配置里的字号字符串转为 Pt;已是 pt 数值时直接使用。"""
64
+ if not size:
65
+ return None
66
+ match = re.search(r"(\d+(?:\.\d+)?)", str(size))
67
+ if not match:
68
+ return None
69
+ return Pt(float(match.group(1)))
70
+
71
+
72
+ def _set_run_font(run, font_name: str, size: Optional[Pt], bold: Optional[bool] = None) -> None:
73
+ run.font.name = font_name
74
+ if size is not None:
75
+ run.font.size = size
76
+ if bold is not None:
77
+ run.font.bold = bold
78
+ rpr = run._element.get_or_add_rPr()
79
+ rfonts = rpr.rFonts
80
+ if rfonts is None:
81
+ rfonts = OxmlElement("w:rFonts")
82
+ rpr.append(rfonts)
83
+ rfonts.set(qn("w:eastAsia"), font_name)
84
+ rfonts.set(qn("w:ascii"), font_name)
85
+ rfonts.set(qn("w:hAnsi"), font_name)
86
+
87
+
88
+ def _set_paragraph_font(paragraph: Paragraph, font_name: str, size: Optional[Pt], bold: Optional[bool] = None) -> None:
89
+ """段落已有文本时,统一设置整段字体。"""
90
+ for run in paragraph.runs:
91
+ _set_run_font(run, font_name, size, bold)
92
+
93
+
94
+ def _set_doc_default_font(doc: DocumentType, font_name: str, size: Optional[Pt]) -> None:
95
+ styles = doc.styles
96
+ if "Normal" in styles:
97
+ normal = styles["Normal"]
98
+ normal.font.name = font_name
99
+ normal.font.size = size
100
+ rpr = normal.element.get_or_add_rPr()
101
+ rfonts = rpr.rFonts
102
+ if rfonts is None:
103
+ rfonts = OxmlElement("w:rFonts")
104
+ rpr.append(rfonts)
105
+ rfonts.set(qn("w:eastAsia"), font_name)
106
+ rfonts.set(qn("w:ascii"), font_name)
107
+ rfonts.set(qn("w:hAnsi"), font_name)
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # 文本处理
112
+ # ---------------------------------------------------------------------------
113
+
114
+ def _split_inline_bold(text: str) -> List[tuple]:
115
+ """切分 ``**加粗**`` 片段,返回 [(文本, 是否加粗), ...]。"""
116
+ parts: List[tuple] = []
117
+ pattern = re.compile(r"\*\*(.+?)\*\*")
118
+ last = 0
119
+ for match in pattern.finditer(text):
120
+ if match.start() > last:
121
+ parts.append((text[last:match.start()], False))
122
+ parts.append((match.group(1), True))
123
+ last = match.end()
124
+ if last < len(text):
125
+ parts.append((text[last:], False))
126
+ if not parts:
127
+ parts.append((text, False))
128
+ return parts
129
+
130
+
131
+ def _add_body_paragraph(doc: DocumentType, text: str, font_name: str, size: Optional[Pt]) -> None:
132
+ paragraph = doc.add_paragraph()
133
+ paragraph.paragraph_format.first_line_indent = Pt(28)
134
+ paragraph.paragraph_format.line_spacing = 1.5
135
+ parts = _split_inline_bold(text)
136
+ for part_text, bold in parts:
137
+ run = paragraph.add_run(part_text)
138
+ _set_run_font(run, font_name, size, bold if bold else None)
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # 主构建逻辑
143
+ # ---------------------------------------------------------------------------
144
+
145
+ def build_document(data: OfficialDocument, output_path: Path, template_bytes: bytes) -> Path:
146
+ """根据结构化内容生成 Word 公文。
147
+
148
+ :param data: 结构化公文内容
149
+ :param output_path: 生成的 docx 写入路径
150
+ :param template_bytes: 包内范文的二进制内容(页面设置 / 样式 / 页眉页脚参考)
151
+ :return: 生成文件路径
152
+ """
153
+ output_path = Path(output_path)
154
+ output_path.parent.mkdir(parents=True, exist_ok=True)
155
+
156
+ doc = Document(io.BytesIO(template_bytes))
157
+
158
+ # 清空模板正文(保留页面设置、样式与页眉页脚)
159
+ body = doc._element.body
160
+ for child in list(body):
161
+ if child.tag != qn("w:sectPr"):
162
+ body.remove(child)
163
+
164
+ _set_doc_default_font(doc, "仿宋_GB2312", _pt("16pt"))
165
+
166
+ # 文号
167
+ if data.document_number:
168
+ num_paragraph = doc.add_paragraph()
169
+ num_paragraph.alignment = 1 # 居中
170
+ num_paragraph.paragraph_format.space_before = Pt(6)
171
+ num_paragraph.paragraph_format.space_after = Pt(12)
172
+ num_run = num_paragraph.add_run(data.document_number)
173
+ _set_run_font(num_run, "仿宋_GB2312", _pt("16pt"))
174
+
175
+ # 标题
176
+ title_paragraph = doc.add_paragraph()
177
+ title_paragraph.alignment = 1 # 居中
178
+ title_paragraph.paragraph_format.space_before = Pt(12)
179
+ title_paragraph.paragraph_format.space_after = Pt(18)
180
+ title_run = title_paragraph.add_run(data.title)
181
+ _set_run_font(title_run, "方正小标宋简体", _pt("22pt"), True)
182
+
183
+ # 主送单位
184
+ if data.recipient:
185
+ recipient_paragraph = doc.add_paragraph()
186
+ recipient_paragraph.paragraph_format.line_spacing = 1.5
187
+ recipient_run = recipient_paragraph.add_run(data.recipient)
188
+ _set_run_font(recipient_run, "仿宋_GB2312", _pt("16pt"))
189
+
190
+ # 导语
191
+ if data.introduction:
192
+ _add_body_paragraph(doc, data.introduction, "仿宋_GB2312", _pt("16pt"))
193
+
194
+ # 章节
195
+ for section in data.sections:
196
+ if section.heading:
197
+ head_paragraph = doc.add_paragraph()
198
+ head_paragraph.paragraph_format.space_before = Pt(12)
199
+ head_paragraph.paragraph_format.space_after = Pt(6)
200
+ head_run = head_paragraph.add_run(section.heading)
201
+ _set_run_font(head_run, "黑体", _pt("16pt"), True)
202
+
203
+ for subheading in section.subheadings:
204
+ sub_paragraph = doc.add_paragraph()
205
+ sub_paragraph.paragraph_format.first_line_indent = Pt(28)
206
+ sub_paragraph.paragraph_format.line_spacing = 1.5
207
+ sub_run = sub_paragraph.add_run(subheading)
208
+ _set_run_font(sub_run, "楷体_GB2312", _pt("16pt"), True)
209
+
210
+ for paragraph_text in section.paragraphs:
211
+ _add_body_paragraph(doc, paragraph_text, "仿宋_GB2312", _pt("16pt"))
212
+
213
+ # 附件
214
+ if data.attachments:
215
+ attach_paragraph = doc.add_paragraph()
216
+ attach_paragraph.paragraph_format.space_before = Pt(12)
217
+ attach_text = "附件:" + "、".join(data.attachments)
218
+ attach_run = attach_paragraph.add_run(attach_text)
219
+ _set_run_font(attach_run, "仿宋_GB2312", _pt("16pt"))
220
+
221
+ # 落款
222
+ if data.issuer or data.department:
223
+ sign_paragraph = doc.add_paragraph()
224
+ sign_paragraph.alignment = 2 # 右对齐
225
+ sign_paragraph.paragraph_format.space_before = Pt(24)
226
+ sign_text = " ".join([part for part in [data.issuer, data.department] if part])
227
+ sign_run = sign_paragraph.add_run(sign_text)
228
+ _set_run_font(sign_run, "仿宋_GB2312", _pt("16pt"))
229
+
230
+ if data.issue_date:
231
+ date_paragraph = doc.add_paragraph()
232
+ date_paragraph.alignment = 2
233
+ date_run = date_paragraph.add_run(data.issue_date)
234
+ _set_run_font(date_run, "仿宋_GB2312", _pt("16pt"))
235
+
236
+ # 抄送
237
+ if data.cc:
238
+ cc_paragraph = doc.add_paragraph()
239
+ cc_paragraph.paragraph_format.space_before = Pt(18)
240
+ cc_run = cc_paragraph.add_run(f"抄送:{data.cc}")
241
+ _set_run_font(cc_run, "仿宋_GB2312", _pt("14pt"))
242
+
243
+ doc.save(str(output_path))
244
+ return output_path
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cscec-gwdoc-mcp"
7
+ version = "0.1.0"
8
+ description = "中建企业公文 Word(.docx)生成 stdio MCP 服务,供百炼函数计算 uvx 托管接入。"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "CSCEC" }]
13
+ keywords = ["mcp", "word", "docx", "公文", "cscec"]
14
+ dependencies = [
15
+ "mcp[cli]>=1.0,<2",
16
+ "python-docx>=1.1",
17
+ "pydantic>=2.0",
18
+ "lxml>=4.9",
19
+ ]
20
+
21
+ [project.scripts]
22
+ cscec-gwdoc-mcp = "cscec_gwdoc_mcp.server:main"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["cscec_gwdoc_mcp"]