@cteno/sdk-linux-x64 0.0.0-stage → 2.10.13
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/LICENSE.daemon +31 -0
- package/README.md +4 -2
- package/agents/flow-analyzer/AGENT.md +71 -0
- package/bin/cteno-host-cli +4 -0
- package/bin/cteno-wiki +0 -0
- package/bin/rg +0 -0
- package/bin/secret-exec +0 -0
- package/manifest.json +12 -0
- package/package.json +33 -4
- package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
- package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
- package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
- package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
- package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
- package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
- package/plugins/cteno.inbox/.cteno-plugin/plugin.json +128 -0
- package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
- package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
- package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
- package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
- package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
- package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
- package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
- package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
- package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
- package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
- package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
- package/plugins/cteno.memory/ui/index.html +822 -0
- package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
- package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
- package/plugins/jev/.cteno-plugin/plugin.json +305 -0
- package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
- package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
- package/skills/agent-create/SKILL.md +86 -0
- package/skills/cteno-host-cli/SKILL.md +248 -0
- package/skills/docx/LICENSE +23 -0
- package/skills/docx/NOTICE.md +8 -0
- package/skills/docx/SKILL.md +69 -0
- package/skills/docx/references/cjk_typography.md +357 -0
- package/skills/docx/references/design_principles.md +819 -0
- package/skills/docx/references/revisions-and-comments.md +88 -0
- package/skills/docx/references/track_changes_guide.md +200 -0
- package/skills/docx/references/typography_guide.md +294 -0
- package/skills/docx/scripts/cjk_fonts.py +244 -0
- package/skills/docx/scripts/docx_comments.py +289 -0
- package/skills/docx/scripts/docx_common.py +92 -0
- package/skills/docx/scripts/docx_create.py +259 -0
- package/skills/docx/scripts/docx_edit.py +250 -0
- package/skills/docx/scripts/docx_read.py +149 -0
- package/skills/docx/scripts/docx_revisions.py +146 -0
- package/skills/docx/scripts/docx_template.py +70 -0
- package/skills/docx/scripts/docx_validate.py +156 -0
- package/skills/docx/scripts/render.py +250 -0
- package/skills/docx/tests/test_docx_skill.py +525 -0
- package/skills/home-devices/SKILL.md +121 -0
- package/skills/mcp/SKILL.md +72 -0
- package/skills/mcp/scripts/mcp_setup.py +439 -0
- package/skills/pdf/LICENSE +23 -0
- package/skills/pdf/NOTICE.md +8 -0
- package/skills/pdf/SKILL.md +63 -0
- package/skills/pdf/references/design-tokens.md +381 -0
- package/skills/pdf/references/forms.md +99 -0
- package/skills/pdf/references/nano-pdf-editing.md +40 -0
- package/skills/pdf/references/ocr-extraction.md +165 -0
- package/skills/pdf/scripts/_raster.py +76 -0
- package/skills/pdf/scripts/extract_marker.py +88 -0
- package/skills/pdf/scripts/extract_pymupdf.py +101 -0
- package/skills/pdf/scripts/pdf_create.py +191 -0
- package/skills/pdf/scripts/pdf_fill_form.py +97 -0
- package/skills/pdf/scripts/pdf_form_layout.py +168 -0
- package/skills/pdf/scripts/pdf_make_form.py +145 -0
- package/skills/pdf/scripts/pdf_merge.py +50 -0
- package/skills/pdf/scripts/pdf_meta.py +115 -0
- package/skills/pdf/scripts/pdf_page_image.py +99 -0
- package/skills/pdf/scripts/pdf_read.py +153 -0
- package/skills/pdf/scripts/pdf_secure.py +71 -0
- package/skills/pdf/scripts/pdf_split.py +84 -0
- package/skills/pdf/scripts/pdf_stamp.py +143 -0
- package/skills/pdf/scripts/pdf_watermark.py +51 -0
- package/skills/pdf/scripts/render.py +250 -0
- package/skills/pdf/tests/test_pdf_skill.py +415 -0
- package/skills/pptx/LICENSE +23 -0
- package/skills/pptx/NOTICE.md +8 -0
- package/skills/pptx/SKILL.md +62 -0
- package/skills/pptx/references/design-system.md +392 -0
- package/skills/pptx/references/pptxgenjs.md +420 -0
- package/skills/pptx/references/slide-types.md +413 -0
- package/skills/pptx/scripts/cjk_fonts.py +244 -0
- package/skills/pptx/scripts/pptx_create.py +240 -0
- package/skills/pptx/scripts/pptx_edit.py +436 -0
- package/skills/pptx/scripts/pptx_from_template.py +88 -0
- package/skills/pptx/scripts/pptx_read.py +131 -0
- package/skills/pptx/scripts/pptx_render.py +96 -0
- package/skills/pptx/scripts/render.py +250 -0
- package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
- package/skills/skill-create/SKILL.md +114 -0
- package/skills/video-generation/SKILL.md +27 -0
- package/skills/xlsx/LICENSE +23 -0
- package/skills/xlsx/NOTICE.md +8 -0
- package/skills/xlsx/SKILL.md +63 -0
- package/skills/xlsx/references/format.md +71 -0
- package/skills/xlsx/references/restructuring.md +71 -0
- package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
- package/skills/xlsx/scripts/formula_check.py +428 -0
- package/skills/xlsx/scripts/render.py +250 -0
- package/skills/xlsx/scripts/xlsx_create.py +265 -0
- package/skills/xlsx/scripts/xlsx_edit.py +269 -0
- package/skills/xlsx/scripts/xlsx_read.py +166 -0
- package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
- package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
- package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
- package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
- package/tools/create_goal/TOOL.md +36 -0
- package/tools/image_generation/TOOL.md +81 -0
- package/tools/skill/TOOL.md +117 -0
- package/tools/update_profile_notes/TOOL.md +39 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: mcp
|
|
3
|
+
name: mcp
|
|
4
|
+
description: "根据用户提供的 MCP 文档(README/配置片段/链接),自动解析、下载安装并注册 MCP Server"
|
|
5
|
+
when_to_use: "用户要求安装/接入/注册 MCP,或给出 MCP 文档、mcpServers JSON、MCP 启动命令时使用"
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
tags:
|
|
8
|
+
- mcp
|
|
9
|
+
- integration
|
|
10
|
+
- install
|
|
11
|
+
- automation
|
|
12
|
+
user_invocable: true
|
|
13
|
+
disable_model_invocation: false
|
|
14
|
+
argument_hint: "<mcp_doc_path_or_url_or_text> [server_name]"
|
|
15
|
+
metadata:
|
|
16
|
+
display-name: "MCP Skill"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# MCP Skill
|
|
20
|
+
|
|
21
|
+
把用户提供的 MCP 文档自动落地为可用 MCP Server:**解析文档 -> 安装依赖 -> 注册到本地 MCPRegistry -> 验证可见**。
|
|
22
|
+
|
|
23
|
+
## 输入
|
|
24
|
+
|
|
25
|
+
- `$ARGS`: MCP 文档来源,可以是:
|
|
26
|
+
- 本地文件路径(README/Markdown/JSON)
|
|
27
|
+
- URL
|
|
28
|
+
- 一段文本(含 `mcpServers` JSON 或启动命令)
|
|
29
|
+
|
|
30
|
+
## 执行规则
|
|
31
|
+
|
|
32
|
+
1. 如果用户已经给了文档路径/URL/文本,直接执行,不要反复追问。
|
|
33
|
+
2. 优先使用本 Skill 自带脚本:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python3 "${SKILL_DIR}/scripts/mcp_setup.py" --input "$ARGS"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. 若文档中有多个 server,优先匹配用户提到的名称;否则使用脚本自动选中的第一个。
|
|
40
|
+
4. 如果安装失败:
|
|
41
|
+
- 先输出失败命令与 stderr 摘要
|
|
42
|
+
- 尝试脚本给出的回退路径(例如 npx/uvx 直启)
|
|
43
|
+
- 仍失败再向用户报告缺失前置依赖(Node/Python/uv 等)
|
|
44
|
+
5. 注册成功后,必须给出:
|
|
45
|
+
- server id
|
|
46
|
+
- transport(stdio/streamable_http;旧 http_sse 配置仍兼容)
|
|
47
|
+
- protocol(legacy 或 auto_2026_07_28;仅在服务端明确支持/需要最新版无状态协议时启用后者)
|
|
48
|
+
- 安装执行结果
|
|
49
|
+
- 注册验证结果(list-mcp-servers 中可见)
|
|
50
|
+
|
|
51
|
+
## 多 server 文档处理
|
|
52
|
+
|
|
53
|
+
当文档里有多个 `mcpServers` 条目时,使用:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 "${SKILL_DIR}/scripts/mcp_setup.py" --input "$ARGS" --server "<server_key>"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 仅预览(不执行安装/注册)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3 "${SKILL_DIR}/scripts/mcp_setup.py" --input "$ARGS" --dry-run
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 结果验收标准
|
|
66
|
+
|
|
67
|
+
完成时满足以下全部条件:
|
|
68
|
+
|
|
69
|
+
1. 已解析出结构化 `config`(含 id/name/transport)。
|
|
70
|
+
2. 已执行安装步骤,或明确说明为何可跳过安装。
|
|
71
|
+
3. `add-mcp-server` RPC 返回成功(或幂等更新成功)。
|
|
72
|
+
4. `list-mcp-servers` 中能看到目标 server。
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parse MCP docs, auto-install dependencies, and register MCP servers via local RPC."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shlex
|
|
11
|
+
import socket
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import urllib.request
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sanitize_id(raw: str) -> str:
|
|
21
|
+
cleaned = re.sub(r"[^a-zA-Z0-9_-]+", "-", raw.strip().lower()).strip("-_")
|
|
22
|
+
if not cleaned:
|
|
23
|
+
cleaned = "mcp-server"
|
|
24
|
+
if cleaned[0].isdigit():
|
|
25
|
+
cleaned = f"mcp-{cleaned}"
|
|
26
|
+
return cleaned
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def socket_path() -> Path:
|
|
30
|
+
override = os.environ.get("CTENO_RPC_SOCKET")
|
|
31
|
+
if override:
|
|
32
|
+
return Path(override)
|
|
33
|
+
|
|
34
|
+
home = Path.home()
|
|
35
|
+
env_tag = os.environ.get("CTENO_ENV", "").strip()
|
|
36
|
+
if env_tag and env_tag != "release":
|
|
37
|
+
return home / ".agents" / f"daemon.{env_tag}.sock"
|
|
38
|
+
|
|
39
|
+
dev_sock = home / ".agents" / "daemon.dev.sock"
|
|
40
|
+
if dev_sock.exists():
|
|
41
|
+
return dev_sock
|
|
42
|
+
return home / ".agents" / "daemon.sock"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rpc_call(method: str, params: dict[str, Any]) -> Any:
|
|
46
|
+
sock = socket_path()
|
|
47
|
+
if not sock.exists():
|
|
48
|
+
raise RuntimeError(f"RPC socket not found: {sock}")
|
|
49
|
+
|
|
50
|
+
req_id = str(uuid.uuid4())
|
|
51
|
+
payload = {
|
|
52
|
+
"id": req_id,
|
|
53
|
+
"method": method,
|
|
54
|
+
"params": params,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
|
58
|
+
client.settimeout(20)
|
|
59
|
+
client.connect(str(sock))
|
|
60
|
+
client.sendall((json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
61
|
+
|
|
62
|
+
chunks: list[bytes] = []
|
|
63
|
+
while True:
|
|
64
|
+
b = client.recv(4096)
|
|
65
|
+
if not b:
|
|
66
|
+
break
|
|
67
|
+
chunks.append(b)
|
|
68
|
+
if b"\n" in b:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if not chunks:
|
|
72
|
+
raise RuntimeError(f"No RPC response for method {method}")
|
|
73
|
+
|
|
74
|
+
line = b"".join(chunks).split(b"\n", 1)[0].decode("utf-8", errors="replace")
|
|
75
|
+
response = json.loads(line)
|
|
76
|
+
|
|
77
|
+
if response.get("id") != req_id:
|
|
78
|
+
raise RuntimeError(f"RPC id mismatch for method {method}")
|
|
79
|
+
if "error" in response and response["error"]:
|
|
80
|
+
raise RuntimeError(f"RPC error ({method}): {response['error']}")
|
|
81
|
+
|
|
82
|
+
return response.get("result")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def strip_json_comments(text: str) -> str:
|
|
86
|
+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
|
87
|
+
text = re.sub(r"(^|\s)//.*$", "", text, flags=re.MULTILINE)
|
|
88
|
+
text = re.sub(r",\s*([}\]])", r"\1", text)
|
|
89
|
+
return text
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def iter_fenced_blocks(text: str) -> list[tuple[str, str]]:
|
|
93
|
+
out: list[tuple[str, str]] = []
|
|
94
|
+
for m in re.finditer(r"```([a-zA-Z0-9_-]*)\n(.*?)```", text, flags=re.DOTALL):
|
|
95
|
+
lang = (m.group(1) or "").strip().lower()
|
|
96
|
+
body = m.group(2)
|
|
97
|
+
out.append((lang, body))
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def try_parse_json(text: str) -> Any | None:
|
|
102
|
+
text = text.strip()
|
|
103
|
+
if not text:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
for candidate in (text, strip_json_comments(text)):
|
|
107
|
+
try:
|
|
108
|
+
return json.loads(candidate)
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_url(value: str) -> bool:
|
|
116
|
+
return value.startswith("http://") or value.startswith("https://")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def read_input_source(source: str) -> tuple[str, str]:
|
|
120
|
+
p = Path(source)
|
|
121
|
+
if p.exists() and p.is_file():
|
|
122
|
+
return p.read_text(encoding="utf-8", errors="replace"), "file"
|
|
123
|
+
|
|
124
|
+
if is_url(source):
|
|
125
|
+
with urllib.request.urlopen(source, timeout=20) as resp:
|
|
126
|
+
data = resp.read().decode("utf-8", errors="replace")
|
|
127
|
+
return data, "url"
|
|
128
|
+
|
|
129
|
+
return source, "text"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def first_non_flag(args: list[str]) -> str | None:
|
|
133
|
+
skip_next = False
|
|
134
|
+
for token in args:
|
|
135
|
+
if skip_next:
|
|
136
|
+
skip_next = False
|
|
137
|
+
continue
|
|
138
|
+
if token in {"-p", "--port", "-c", "--config", "--env-file"}:
|
|
139
|
+
skip_next = True
|
|
140
|
+
continue
|
|
141
|
+
if token.startswith("-"):
|
|
142
|
+
continue
|
|
143
|
+
return token
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def derive_install_commands(command: str, args: list[str]) -> list[list[str]]:
|
|
148
|
+
cmd = Path(command).name.lower()
|
|
149
|
+
pkg = first_non_flag(args)
|
|
150
|
+
if not pkg:
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
if cmd == "npx":
|
|
154
|
+
return [["npm", "install", "-g", pkg]]
|
|
155
|
+
if cmd == "bunx":
|
|
156
|
+
return [["bun", "add", "-g", pkg]]
|
|
157
|
+
if cmd == "uvx":
|
|
158
|
+
return [["uv", "tool", "install", pkg]]
|
|
159
|
+
if cmd in {"python", "python3"} and len(args) >= 2 and args[0] == "-m":
|
|
160
|
+
return [["python3", "-m", "pip", "install", "-U", args[1]]]
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_config_from_entry(server_key: str, entry: dict[str, Any]) -> dict[str, Any]:
|
|
165
|
+
server_id = sanitize_id(server_key)
|
|
166
|
+
name = str(entry.get("name") or server_key)
|
|
167
|
+
enabled = bool(entry.get("enabled", True))
|
|
168
|
+
protocol = str(entry.get("protocol") or "legacy")
|
|
169
|
+
transport = entry.get("transport") if isinstance(entry.get("transport"), dict) else entry
|
|
170
|
+
transport_type = transport.get("type")
|
|
171
|
+
|
|
172
|
+
if isinstance(transport.get("url"), str) and transport["url"].strip():
|
|
173
|
+
if transport_type not in {None, "streamable_http", "http_sse"}:
|
|
174
|
+
raise RuntimeError(f"Unsupported HTTP transport type: {transport_type}")
|
|
175
|
+
return {
|
|
176
|
+
"id": server_id,
|
|
177
|
+
"name": name,
|
|
178
|
+
"enabled": enabled,
|
|
179
|
+
"protocol": protocol,
|
|
180
|
+
"transport": {
|
|
181
|
+
"type": "streamable_http",
|
|
182
|
+
"url": transport["url"].strip(),
|
|
183
|
+
"headers": transport.get("headers", {}) if isinstance(transport.get("headers"), dict) else {},
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if isinstance(transport.get("command"), str) and transport["command"].strip():
|
|
188
|
+
if transport_type not in {None, "stdio"}:
|
|
189
|
+
raise RuntimeError(f"Unsupported stdio transport type: {transport_type}")
|
|
190
|
+
args = transport.get("args", []) if isinstance(transport.get("args"), list) else []
|
|
191
|
+
env = transport.get("env", {}) if isinstance(transport.get("env"), dict) else {}
|
|
192
|
+
return {
|
|
193
|
+
"id": server_id,
|
|
194
|
+
"name": name,
|
|
195
|
+
"enabled": enabled,
|
|
196
|
+
"protocol": protocol,
|
|
197
|
+
"transport": {
|
|
198
|
+
"type": "stdio",
|
|
199
|
+
"command": transport["command"].strip(),
|
|
200
|
+
"args": [str(x) for x in args],
|
|
201
|
+
"env": {str(k): str(v) for k, v in env.items()},
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
raise RuntimeError(f"Server entry '{server_key}' has neither 'command' nor 'url'")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def parse_from_json_obj(obj: Any, server_hint: str | None) -> tuple[dict[str, Any], str] | None:
|
|
209
|
+
if not isinstance(obj, dict):
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
mcp_servers = obj.get("mcpServers")
|
|
213
|
+
if isinstance(mcp_servers, dict) and mcp_servers:
|
|
214
|
+
if server_hint and server_hint in mcp_servers:
|
|
215
|
+
key = server_hint
|
|
216
|
+
else:
|
|
217
|
+
key = next(iter(mcp_servers.keys()))
|
|
218
|
+
|
|
219
|
+
entry = mcp_servers.get(key)
|
|
220
|
+
if isinstance(entry, dict):
|
|
221
|
+
return build_config_from_entry(str(key), entry), f"json:mcpServers:{key}"
|
|
222
|
+
|
|
223
|
+
if isinstance(obj.get("command"), str) or isinstance(obj.get("url"), str):
|
|
224
|
+
key = server_hint or str(obj.get("name") or "mcp-server")
|
|
225
|
+
return build_config_from_entry(key, obj), "json:single"
|
|
226
|
+
|
|
227
|
+
transport = obj.get("transport")
|
|
228
|
+
if isinstance(transport, dict) and isinstance(obj.get("id"), str) and isinstance(obj.get("name"), str):
|
|
229
|
+
return build_config_from_entry(str(obj["id"]), obj), "json:transport"
|
|
230
|
+
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def parse_from_command_lines(text: str) -> tuple[dict[str, Any], str] | None:
|
|
235
|
+
candidates: list[str] = []
|
|
236
|
+
for lang, body in iter_fenced_blocks(text):
|
|
237
|
+
if lang in {"bash", "sh", "zsh", "shell", ""}:
|
|
238
|
+
candidates.extend(body.splitlines())
|
|
239
|
+
|
|
240
|
+
if not candidates:
|
|
241
|
+
candidates = text.splitlines()
|
|
242
|
+
|
|
243
|
+
preferred = []
|
|
244
|
+
fallback = []
|
|
245
|
+
for raw in candidates:
|
|
246
|
+
line = raw.strip()
|
|
247
|
+
if not line or line.startswith("#"):
|
|
248
|
+
continue
|
|
249
|
+
if re.match(r"^(npx|bunx|uvx|python3?|node)\b", line):
|
|
250
|
+
if "mcp" in line.lower():
|
|
251
|
+
preferred.append(line)
|
|
252
|
+
else:
|
|
253
|
+
fallback.append(line)
|
|
254
|
+
|
|
255
|
+
line = preferred[0] if preferred else (fallback[0] if fallback else None)
|
|
256
|
+
if not line:
|
|
257
|
+
inline = re.search(
|
|
258
|
+
r"(npx|bunx|uvx|python3?|node)\s+[^\n`]+",
|
|
259
|
+
text,
|
|
260
|
+
flags=re.IGNORECASE,
|
|
261
|
+
)
|
|
262
|
+
if inline:
|
|
263
|
+
line = inline.group(0).strip()
|
|
264
|
+
if not line:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
parts = shlex.split(line)
|
|
268
|
+
if not parts:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
command = parts[0]
|
|
272
|
+
args = parts[1:]
|
|
273
|
+
key = first_non_flag(args) or Path(command).name
|
|
274
|
+
key = sanitize_id(key.replace("@", "").replace("/", "-"))
|
|
275
|
+
|
|
276
|
+
config = {
|
|
277
|
+
"id": key,
|
|
278
|
+
"name": key,
|
|
279
|
+
"enabled": True,
|
|
280
|
+
"transport": {
|
|
281
|
+
"type": "stdio",
|
|
282
|
+
"command": command,
|
|
283
|
+
"args": args,
|
|
284
|
+
"env": {},
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
return config, f"command:{line}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def parse_mcp_doc(text: str, server_hint: str | None) -> tuple[dict[str, Any], str]:
|
|
291
|
+
parsed_full = try_parse_json(text)
|
|
292
|
+
if parsed_full is not None:
|
|
293
|
+
out = parse_from_json_obj(parsed_full, server_hint)
|
|
294
|
+
if out:
|
|
295
|
+
return out
|
|
296
|
+
|
|
297
|
+
for lang, body in iter_fenced_blocks(text):
|
|
298
|
+
if lang in {"json", "jsonc", "javascript", "js", "ts", ""}:
|
|
299
|
+
obj = try_parse_json(body)
|
|
300
|
+
if obj is None:
|
|
301
|
+
continue
|
|
302
|
+
out = parse_from_json_obj(obj, server_hint)
|
|
303
|
+
if out:
|
|
304
|
+
return out
|
|
305
|
+
|
|
306
|
+
cmd_out = parse_from_command_lines(text)
|
|
307
|
+
if cmd_out:
|
|
308
|
+
return cmd_out
|
|
309
|
+
|
|
310
|
+
raise RuntimeError(
|
|
311
|
+
"Could not extract MCP config from input. Provide mcpServers JSON or a runnable command."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def maybe_install(config: dict[str, Any], dry_run: bool, continue_on_error: bool) -> list[dict[str, Any]]:
|
|
316
|
+
transport = config.get("transport", {})
|
|
317
|
+
if not isinstance(transport, dict):
|
|
318
|
+
return []
|
|
319
|
+
if transport.get("type") != "stdio":
|
|
320
|
+
return []
|
|
321
|
+
|
|
322
|
+
command = str(transport.get("command") or "")
|
|
323
|
+
args = transport.get("args", []) if isinstance(transport.get("args"), list) else []
|
|
324
|
+
install_cmds = derive_install_commands(command, [str(x) for x in args])
|
|
325
|
+
|
|
326
|
+
results: list[dict[str, Any]] = []
|
|
327
|
+
for cmd in install_cmds:
|
|
328
|
+
item: dict[str, Any] = {"command": " ".join(shlex.quote(x) for x in cmd)}
|
|
329
|
+
if dry_run:
|
|
330
|
+
item["status"] = "skipped(dry-run)"
|
|
331
|
+
results.append(item)
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
proc = subprocess.run(cmd, text=True, capture_output=True)
|
|
335
|
+
item["status"] = "ok" if proc.returncode == 0 else "failed"
|
|
336
|
+
item["exit_code"] = proc.returncode
|
|
337
|
+
item["stdout_tail"] = "\n".join(proc.stdout.splitlines()[-10:])
|
|
338
|
+
item["stderr_tail"] = "\n".join(proc.stderr.splitlines()[-10:])
|
|
339
|
+
results.append(item)
|
|
340
|
+
|
|
341
|
+
if proc.returncode != 0 and not continue_on_error:
|
|
342
|
+
raise RuntimeError(
|
|
343
|
+
f"Install command failed: {item['command']}\n{item.get('stderr_tail', '')}".strip()
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return results
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def register_server(config: dict[str, Any], dry_run: bool) -> dict[str, Any]:
|
|
350
|
+
server_id = config["id"]
|
|
351
|
+
|
|
352
|
+
if dry_run:
|
|
353
|
+
return {
|
|
354
|
+
"status": "skipped(dry-run)",
|
|
355
|
+
"server_id": server_id,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
listed = rpc_call("list-mcp-servers", {})
|
|
359
|
+
servers = listed.get("servers", []) if isinstance(listed, dict) else []
|
|
360
|
+
exists = any(isinstance(s, dict) and s.get("id") == server_id for s in servers)
|
|
361
|
+
|
|
362
|
+
if exists:
|
|
363
|
+
remove_res = rpc_call("remove-mcp-server", {"serverId": server_id})
|
|
364
|
+
if isinstance(remove_res, dict) and remove_res.get("success") is False:
|
|
365
|
+
raise RuntimeError(remove_res.get("error") or f"Failed to remove existing server '{server_id}'")
|
|
366
|
+
|
|
367
|
+
add_res = rpc_call("add-mcp-server", config)
|
|
368
|
+
if isinstance(add_res, dict) and add_res.get("success") is False:
|
|
369
|
+
raise RuntimeError(add_res.get("error") or f"Failed to add server '{server_id}'")
|
|
370
|
+
|
|
371
|
+
listed_after = rpc_call("list-mcp-servers", {})
|
|
372
|
+
servers_after = listed_after.get("servers", []) if isinstance(listed_after, dict) else []
|
|
373
|
+
visible = any(isinstance(s, dict) and s.get("id") == server_id for s in servers_after)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
"status": "ok" if visible else "warning:not-visible",
|
|
377
|
+
"server_id": server_id,
|
|
378
|
+
"already_existed": exists,
|
|
379
|
+
"add_result": add_res,
|
|
380
|
+
"visible_in_list": visible,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def main() -> int:
|
|
385
|
+
parser = argparse.ArgumentParser(description="Auto setup MCP server from docs.")
|
|
386
|
+
parser.add_argument("--input", required=True, help="Path/URL/raw text containing MCP docs")
|
|
387
|
+
parser.add_argument("--server", help="Server key when mcpServers has multiple entries")
|
|
388
|
+
parser.add_argument("--id", dest="override_id", help="Override server id")
|
|
389
|
+
parser.add_argument("--name", dest="override_name", help="Override server display name")
|
|
390
|
+
parser.add_argument("--skip-install", action="store_true", help="Skip dependency installation")
|
|
391
|
+
parser.add_argument("--skip-register", action="store_true", help="Skip RPC registration")
|
|
392
|
+
parser.add_argument("--continue-on-install-error", action="store_true", help="Continue even when install fails")
|
|
393
|
+
parser.add_argument("--dry-run", action="store_true", help="Parse only; do not install/register")
|
|
394
|
+
args = parser.parse_args()
|
|
395
|
+
|
|
396
|
+
result: dict[str, Any] = {
|
|
397
|
+
"ok": False,
|
|
398
|
+
"input": args.input,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
text, input_kind = read_input_source(args.input)
|
|
403
|
+
config, parsed_from = parse_mcp_doc(text, args.server)
|
|
404
|
+
|
|
405
|
+
if args.override_id:
|
|
406
|
+
config["id"] = sanitize_id(args.override_id)
|
|
407
|
+
if args.override_name:
|
|
408
|
+
config["name"] = args.override_name
|
|
409
|
+
|
|
410
|
+
result["input_kind"] = input_kind
|
|
411
|
+
result["parsed_from"] = parsed_from
|
|
412
|
+
result["config"] = config
|
|
413
|
+
|
|
414
|
+
if args.skip_install:
|
|
415
|
+
result["install"] = {"status": "skipped"}
|
|
416
|
+
else:
|
|
417
|
+
installs = maybe_install(config, dry_run=args.dry_run, continue_on_error=args.continue_on_install_error)
|
|
418
|
+
result["install"] = {
|
|
419
|
+
"status": "ok",
|
|
420
|
+
"steps": installs,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if args.skip_register:
|
|
424
|
+
result["register"] = {"status": "skipped"}
|
|
425
|
+
else:
|
|
426
|
+
result["register"] = register_server(config, dry_run=args.dry_run)
|
|
427
|
+
|
|
428
|
+
result["ok"] = True
|
|
429
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
430
|
+
return 0
|
|
431
|
+
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
result["error"] = str(exc)
|
|
434
|
+
print(json.dumps(result, ensure_ascii=False, indent=2), file=sys.stderr)
|
|
435
|
+
return 1
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if __name__ == "__main__":
|
|
439
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cteno
|
|
4
|
+
Portions Copyright (c) 2026 Nous Research (scripts/ and tests/ adapted from the Hermes Agent productivity skills)
|
|
5
|
+
Portions Copyright (c) 2026 MiniMaxAI (references adapted from MiniMax-AI/skills)
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Sources for the office skills (docx, pptx, xlsx, pdf)
|
|
2
|
+
|
|
3
|
+
- **Scripts and tests**: adapted from Nous Research Hermes Agent `skills/productivity/{docx,powerpoint,xlsx,pdf}` (MIT), upstream commit `bf0203a` (2026-10-22).
|
|
4
|
+
- **Chinese typesetting, design and formatting references**: adapted from MiniMax-AI/skills `minimax-docx`, `pptx-generator`, `minimax-xlsx`, `minimax-pdf` (MIT); `xlsx/scripts/formula_check.py` from minimax-xlsx.
|
|
5
|
+
- **Cteno additions**: `scripts/render.py` (isolated LibreOffice profile, macOS CJK font exposure and substitution), `scripts/cjk_fonts.py` (East Asian font slots), CJK handling in `docx_create.py` / `pptx_create.py` / `pdf_create.py`, 16:9 layout stretch in `pptx_create.py`, all `SKILL.md` texts.
|
|
6
|
+
- Anthropic's document skills (proprietary licence) informed which pitfalls to cover; no text or code was copied from them.
|
|
7
|
+
|
|
8
|
+
Not tracked live: to update, diff the upstream folders above against these copies and re-run each skill's `tests/` plus a render check.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pdf
|
|
3
|
+
name: pdf
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Create, read and manipulate PDFs: designed reports from HTML or a JSON spec (Chinese supported), text/table extraction, OCR of scans, merge/split/rotate, watermark/stamp, fillable forms (build, fill, flatten), metadata/attachments, encryption, and office-to-PDF conversion."
|
|
6
|
+
when_to_use: "Use whenever a .pdf is input or output: make a PDF report/resume/proposal/invoice, export Word/PPT/Excel to PDF, extract text or tables, OCR a scanned PDF, merge/split/rotate pages, add a watermark or signature image, fill or create a PDF form, encrypt/decrypt. For an editable Word deliverable use docx first, then convert."
|
|
7
|
+
license: MIT (see LICENSE; scripts adapted from Nous Research and MiniMax, MIT)
|
|
8
|
+
tags: [pdf, report, forms, ocr, merge, watermark]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# PDF
|
|
12
|
+
|
|
13
|
+
## Choose the creation route by what the PDF must look like
|
|
14
|
+
|
|
15
|
+
| Need | Route |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Already have a .docx/.pptx/.xlsx | `python3 ${SKILL_DIR}/scripts/render.py FILE --pdf-only` (LibreOffice; same as the office skills' preview) |
|
|
18
|
+
| Designed report, resume, brochure, invoice with real layout | Write **HTML + CSS** (print styles, `@page` size and margins, page breaks) and print it with the `browser` tool / headless Chrome (`page.pdf()`), or `weasyprint` if installed. Best typography, CJK just works with system fonts. See `${SKILL_DIR}/references/design-tokens.md` for palette/type choices |
|
|
19
|
+
| Simple structured document (headings, paragraphs, tables, images) | JSON spec → `${SKILL_DIR}/scripts/pdf_create.py spec.json -o out.pdf` (reportlab; Chinese is detected and a CJK font is embedded automatically, lines break between characters) |
|
|
20
|
+
| Fillable form | `${SKILL_DIR}/scripts/pdf_form_layout.py spec.json` (lint) → `${SKILL_DIR}/scripts/pdf_make_form.py spec.json -o form.pdf` — read `${SKILL_DIR}/references/forms.md` |
|
|
21
|
+
|
|
22
|
+
## Read and manipulate
|
|
23
|
+
|
|
24
|
+
| Task | Command |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Inspect first (pages, encrypted, scanned?) | `${SKILL_DIR}/scripts/pdf_read.py f.pdf --meta` |
|
|
27
|
+
| Text per page / tables → JSON+CSV | `${SKILL_DIR}/scripts/pdf_read.py f.pdf --text` / `--tables --csv-dir t/` |
|
|
28
|
+
| Scanned pages / OCR | read `${SKILL_DIR}/references/ocr-extraction.md` (`${SKILL_DIR}/scripts/extract_pymupdf.py`, `${SKILL_DIR}/scripts/extract_marker.py`) — never report a scan as "no content" |
|
|
29
|
+
| Merge (with bookmarks) | `${SKILL_DIR}/scripts/pdf_merge.py a.pdf b.pdf -o m.pdf [--bookmarks]` |
|
|
30
|
+
| Split / extract / rotate / compress | `${SKILL_DIR}/scripts/pdf_split.py f.pdf --pages 1-3,7 -o part.pdf [--rotate 90] [--compress]` |
|
|
31
|
+
| Watermark with a PDF page | `${SKILL_DIR}/scripts/pdf_watermark.py f.pdf --stamp mark.pdf -o out.pdf [--under]` |
|
|
32
|
+
| Stamp text or image (DRAFT, signature, seal) | `${SKILL_DIR}/scripts/pdf_stamp.py f.pdf -o out.pdf --text "草稿" --x 150 --y 400 --rotation 45 --opacity 0.3` / `--image sig.png --x 400 --y 60 --width 120` |
|
|
33
|
+
| List / fill / flatten form fields | `${SKILL_DIR}/scripts/pdf_read.py f.pdf --fields`, `${SKILL_DIR}/scripts/pdf_fill_form.py f.pdf --fields-json v.json -o out.pdf [--flatten]` |
|
|
34
|
+
| Metadata, attachments | `${SKILL_DIR}/scripts/pdf_meta.py` |
|
|
35
|
+
| Encrypt / decrypt (AES-256) | `${SKILL_DIR}/scripts/pdf_secure.py` |
|
|
36
|
+
| Page images | `${SKILL_DIR}/scripts/pdf_page_image.py f.pdf --pages 1-3 --dpi 150 --out-dir imgs/` or `${SKILL_DIR}/scripts/render.py f.pdf` |
|
|
37
|
+
| Edit text inside an existing PDF | read `${SKILL_DIR}/references/nano-pdf-editing.md`; often better: regenerate from source |
|
|
38
|
+
|
|
39
|
+
Scripts take `--help`, print JSON, exit non-zero on failure; pages are 1-based. Always call scripts by the absolute paths shown here (not a copy found elsewhere on disk).
|
|
40
|
+
Dependencies: `python3 -m pip install pypdf reportlab pdfplumber pypdfium2` (OCR extras per reference file).
|
|
41
|
+
|
|
42
|
+
## Design (documents you create)
|
|
43
|
+
|
|
44
|
+
- Choose palette and type from the content, not "safe" defaults (`${SKILL_DIR}/references/design-tokens.md`): one accent color, one heading face, one body face; A4 for China/Europe, Letter for the US; margins ≥ 20 mm.
|
|
45
|
+
- Chinese text: body 10.5–12 pt with 1.6–1.8 line height; headings 黑体/思源黑体, body 宋体/思源宋体 or 苹方/雅黑 for screen reading; full-width punctuation.
|
|
46
|
+
- Cover pages and TOCs only for long documents (> 6 pages). Tables: header row shaded, numbers right-aligned, repeat header on page breaks.
|
|
47
|
+
- Page numbers on every page except the cover; "第 X 页 / 共 Y 页" for Chinese.
|
|
48
|
+
|
|
49
|
+
## Pitfalls
|
|
50
|
+
|
|
51
|
+
- **Fonts**: reportlab's built-in Helvetica has no Chinese glyphs — text becomes black boxes. `pdf_create.py` handles this; in your own reportlab code register a TrueType CJK font (`TTFont`) and set `wordWrap="CJK"`. Hiragino/PingFang on macOS are CFF and can't be embedded by reportlab — use STHeiti/Songti, Windows 微软雅黑/宋体, or Noto Sans/Serif CJK.
|
|
52
|
+
- **Scanned PDFs** have no text layer: empty `--text` plus page images means OCR, not "empty document".
|
|
53
|
+
- **Encrypted inputs** must be decrypted first (`pdf_secure.py --decrypt`).
|
|
54
|
+
- **Owner-password permissions** (no-print/no-copy) are advisory; only a user password protects content. Don't present them as security.
|
|
55
|
+
- **Form values in CJK** are stored correctly but a field's default font may not show them; verify with `--fields` and flatten with an embedded CJK font if appearance matters.
|
|
56
|
+
- **Table extraction is heuristic** — borderless or merged-cell tables need checking against the page image.
|
|
57
|
+
- **PDF/A** is not produced by these libraries; use Ghostscript + veraPDF if archival conformance is required.
|
|
58
|
+
|
|
59
|
+
## Verify before delivery (required)
|
|
60
|
+
|
|
61
|
+
1. `python3 ${SKILL_DIR}/scripts/pdf_read.py out.pdf --meta` — page count, sizes, encryption as intended.
|
|
62
|
+
2. `python3 ${SKILL_DIR}/scripts/render.py out.pdf` and **open every page image** (or the pages you changed): black boxes or missing CJK glyphs, text past margins, tables split badly, blank trailing page, wrong page numbers, watermark covering content.
|
|
63
|
+
3. After extraction/OCR: spot-check a known string or table cell against the page image. After form fill: re-read `--fields` and compare values exactly.
|