Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .claude-plugin/marketplace.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ This file provides guidance to Claude Code when working with code in this reposi

**macdoc** 是一個原生 macOS 文件處理工具集,專注於文件格式解析、轉換和 OCR 功能。整個專案使用 Swift 開發,充分利用 Apple 平台的原生能力。

本 repo 同時是 **Claude Code plugin marketplace**(`.claude-plugin/marketplace.json` + `plugins/`,2026-07 起,#112):發布 `che-word-mcp`、`che-pdf-mcp`、`che-pptx-mcp`、`macdoc` 四個 plugins,使用者以 `claude plugin marketplace add PsychQuant/macdoc` 安裝。注意 `plugins/`(plugin shells,正常入版控)與 `packages/`(gitignored 本地套件)的差異;MCP shells 的 wrapper 從各 binary repo 的 GitHub Releases 自動下載 binary,安裝前強制驗證 sha256 + Developer ID 簽章鏈(requirement-based codesign,Team `6W377FS7BS`)。發布新版時同步 bump `plugins/<name>/.claude-plugin/plugin.json` 與 `.claude-plugin/marketplace.json` 兩處版本。

## Project Structure

```
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

原生 macOS 文件處理工具集,專注於文件格式轉換和 OCR。使用 Swift 開發,充分利用 Apple 平台原生能力(PDFKit、Vision.framework)。

## Claude Code Plugin Marketplace

本 repo 同時是 Claude Code plugin marketplace,提供 macdoc 生態系的 4 個 plugins:

| Plugin | 內容 |
|--------|------|
| `che-word-mcp` | Word (.docx) MCP server(OOXML 讀寫,218+ 工具) |
| `che-pdf-mcp` | PDF MCP server(PDFKit 解析、Vision OCR) |
| `che-pptx-mcp` | PowerPoint (.pptx) MCP server(PresentationML 解析與生成) |
| `macdoc` | macdoc CLI 使用指南 skill |

```bash
claude plugin marketplace add PsychQuant/macdoc
claude plugin install che-word-mcp@macdoc # 或 che-pdf-mcp / che-pptx-mcp / macdoc
```

MCP plugins 的 wrapper 會自動從各 repo 的 GitHub Releases 下載 universal binary,並在安裝前**強制驗證** sha256 與 Developer ID 簽章鏈(Team `6W377FS7BS`);驗證不過即拒裝。

> 遷移註記:`che-word-mcp` 與 `macdoc` 兩個 plugins 原先發布於 `psychquant-claude-plugins` marketplace,自 2026-07 起以本 marketplace 為準。

## Prerequisites

- **macOS 14+**(Sonoma 或更新)
Expand Down
2 changes: 1 addition & 1 deletion mcp/che-pdf-mcp
8 changes: 8 additions & 0 deletions plugins/che-pdf-mcp/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "che-pdf-mcp",
"description": "PDF 文件處理 MCP server — PDFKit 解析與文字提取、Vision OCR(原生 macOS)、圖片/區域擷取、亂碼區域偵測。 v0.1.0: 首次 marketplace 發布。",
"version": "0.1.0",
"author": {
"name": "Che Cheng"
}
}
7 changes: 7 additions & 0 deletions plugins/che-pdf-mcp/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"pdf": {
"type": "stdio",
"command": "${CLAUDE_PLUGIN_ROOT}/bin/che-pdf-mcp-wrapper.sh",
"description": "PDF 文件處理 MCP server — PDFKit 解析與文字提取、Vision OCR(原生 macOS)、圖片/區域擷取、亂碼區域偵測。"
}
}
13 changes: 13 additions & 0 deletions plugins/che-pdf-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

All notable changes to the che-pdf-mcp plugin shell will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [0.1.0] - 2026-07-02

### Added

- 首次 marketplace 發布(PsychQuant/macdoc marketplace,Refs PsychQuant/macdoc#112)。
- Wrapper 供應鏈驗證(#112 security review R1+R2):sha256 asset 比對為**強制**(缺失/格式錯/mismatch 均拒裝,integrity gate)+ requirement-based `codesign` 驗證鏈定 Apple anchor + Team OU `6W377FS7BS`(authenticity gate — 取代可被 Identifier 欄位偽造的 grep 形式)+ pinned version 不 fallback latest + `curl -f --proto '=https'` + mktemp 唯一暫存檔。驗證失敗一律保留既有 binary(fail-to-known-good)。
- `.mcp.json` + version-aware auto-download wrapper(自 `PsychQuant/che-pdf-mcp` GitHub Releases 下載 signed + notarized universal binary)。
16 changes: 16 additions & 0 deletions plugins/che-pdf-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# che-pdf-mcp

PDF 文件處理 MCP server — PDFKit 解析與文字提取、Vision OCR(原生 macOS)、圖片/區域擷取、亂碼區域偵測。

## 安裝

```bash
claude plugin marketplace add PsychQuant/macdoc
claude plugin install che-pdf-mcp@macdoc
```

Wrapper 會自動從 [GitHub Releases](https://github.com/PsychQuant/che-pdf-mcp/releases) 下載 release 的 `ChePDFMCP` universal binary 到 `~/bin/`,安裝前與每次啟動時強制驗證 sha256(安裝時)與 Developer ID Application 簽章鏈(Team `6W377FS7BS`)。release 流程含 Apple notarization(wrapper 不重複檢查 notarization)。

## 原始碼

https://github.com/PsychQuant/che-pdf-mcp
149 changes: 149 additions & 0 deletions plugins/che-pdf-mcp/bin/che-pdf-mcp-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/bin/bash
# Version-aware auto-download wrapper for ChePDFMCP.
#
# Design:
# - Reads desired version from plugin.json (plugin's intended binary version)
# - Compares against ~/bin/.ChePDFMCP.version sidecar
# - Re-downloads when plugin has been updated but binary is stale
# - Unique temp file (mktemp, same fs) + atomic mv so partial downloads never break things
# - Pinned version does NOT fall back to releases/latest (supply-chain pinning);
# latest is used only when plugin.json carries no version
#
# Supply-chain verification (PsychQuant/macdoc#112 security review R1+R2):
# - sha256 (MANDATORY): release must ship ChePDFMCP.sha256; missing/malformed/
# mismatching asset refuses install (fail-closed integrity gate)
# - Code signature (AUTHENTICITY): requirement-based codesign check pins the
# Apple chain + Team OU 6W377FS7BS. NOTE: a grep on `codesign -dvv` output is
# spoofable via the attacker-controlled Identifier field, and --verify alone
# accepts ad-hoc signatures (empirically reproduced in #112 verify round 1) —
# only the -R requirement form is sound.
# - On any verification failure: keep + exec the existing binary if present
# (fail-to-known-good), else exit 1.

set -u

REPO="PsychQuant/che-pdf-mcp"
BINARY_NAME="ChePDFMCP"
INSTALL_DIR="$HOME/bin"
BINARY="$INSTALL_DIR/$BINARY_NAME"
VERSION_FILE="$INSTALL_DIR/.${BINARY_NAME}.version"
SCRIPT_ARGS=("$@")

# Locate plugin root via wrapper's own path (more reliable than $CLAUDE_PLUGIN_ROOT
# which isn't guaranteed in MCP spawn env). Wrapper lives at PLUGIN_ROOT/bin/*.sh.
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PLUGIN_JSON="$PLUGIN_ROOT/.claude-plugin/plugin.json"

verify_binary() {
# Developer ID Application (marker OIDs) + Team OU pin. Runs on every
# candidate before exec — download-time AND exec-time (#112 verify R2:
# binaries installed by the pre-hardening wrapper — including ad-hoc
# ones — carry a matching sidecar and would otherwise never be re-checked).
codesign --verify --strict \
-R '=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "6W377FS7BS"' \
"$1" 2>/dev/null
}

run_existing_or_die() {
# $1 = error message. Fail-to-VERIFIED-good: prefer the already-installed
# binary over aborting the MCP server spawn — but only if it passes the
# same signature gate as a fresh download.
echo "$BINARY_NAME: ERROR — $1" >&2
rm -f "${TMP_FILE:-}" 2>/dev/null # trap EXIT does not fire across exec — clean up rejected download here
if [[ -x "$BINARY" ]] && verify_binary "$BINARY"; then
echo "$BINARY_NAME: keeping existing (signature-verified) binary" >&2
exec "$BINARY" ${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}
fi
exit 1
}

# Read desired version from plugin.json (empty string on any failure → latest).
DESIRED_VERSION=""
if [[ -f "$PLUGIN_JSON" ]]; then
DESIRED_VERSION=$(grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' "$PLUGIN_JSON" 2>/dev/null \
| head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true)
if [[ -z "$DESIRED_VERSION" ]]; then
# plugin.json exists but version unparseable — fail closed rather than
# silently degrading to the unpinned latest channel (#112 verify R2).
run_existing_or_die "cannot parse version from plugin.json — refusing unpinned download"
fi
fi

# Read currently installed version from sidecar (empty string if missing).
INSTALLED_VERSION=""
[[ -f "$VERSION_FILE" ]] && INSTALLED_VERSION=$(tr -d '[:space:]' < "$VERSION_FILE" 2>/dev/null || true)

# Decide whether to download.
NEED_DOWNLOAD=false
REASON=""
if [[ ! -x "$BINARY" ]]; then
NEED_DOWNLOAD=true
REASON="binary not installed"
elif [[ -n "$DESIRED_VERSION" ]] && [[ "$INSTALLED_VERSION" != "$DESIRED_VERSION" ]]; then
NEED_DOWNLOAD=true
REASON="plugin wants v${DESIRED_VERSION}, installed is v${INSTALLED_VERSION:-unknown}"
fi

if $NEED_DOWNLOAD; then
echo "$BINARY_NAME: $REASON — downloading from $REPO..." >&2
mkdir -p "$INSTALL_DIR"

# Resolve release via the API-free direct-download endpoints (unauthenticated
# api.github.com is rate-limited to 60 req/hr per IP and fails closed here;
# the /releases/download/ redirect endpoints have no such limit).
# Pinned version does NOT fall back to latest — a missing pinned tag is a
# release-channel fault, not a downgrade licence.
if [[ -n "$DESIRED_VERSION" ]]; then
URL="https://github.com/$REPO/releases/download/v$DESIRED_VERSION/$BINARY_NAME"
TARGET_DESC="v$DESIRED_VERSION"
else
URL="https://github.com/$REPO/releases/latest/download/$BINARY_NAME"
TARGET_DESC="latest"
fi

TMP_FILE=$(mktemp "$INSTALL_DIR/.${BINARY_NAME}.download.XXXXXX") || run_existing_or_die "mktemp failed"
trap 'rm -f "$TMP_FILE"' EXIT

# NOTE: successful downloads redirect to the release-assets CDN, so the
# effective URL does NOT expose the tag — the sidecar records the pinned
# version (all shipped plugins pin one; the latest path records "unknown").
curl -fsSL --proto '=https' --tlsv1.2 --max-time 300 "$URL" -o "$TMP_FILE" 2>/dev/null \
|| run_existing_or_die "download failed for $TARGET_DESC at $REPO (pinned versions do not fall back to latest). Install manually: https://github.com/$REPO/releases"

# 1. sha256 — mandatory fail-closed integrity gate.
EXPECTED_SHA=$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "${URL}.sha256" 2>/dev/null \
| head -1 | awk '{print $1}')
[[ "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{64}$ ]] \
|| run_existing_or_die "missing/malformed .sha256 release asset — refusing to install"
ACTUAL_SHA=$(shasum -a 256 "$TMP_FILE" | awk '{print $1}')
[[ "$ACTUAL_SHA" == "$EXPECTED_SHA" ]] \
|| run_existing_or_die "sha256 mismatch against release asset — refusing to install"

# 2. Code signature — requirement-based authenticity gate (see header).
verify_binary "$TMP_FILE" \
|| run_existing_or_die "code-signature verification failed (not a Developer ID Application cert of Team 6W377FS7BS) — refusing to install"

chmod +x "$TMP_FILE" || run_existing_or_die "chmod failed"
mv "$TMP_FILE" "$BINARY" || run_existing_or_die "install mv failed"
trap - EXIT
echo "${DESIRED_VERSION:-unknown}" > "$VERSION_FILE" \
|| echo "$BINARY_NAME: WARNING — version sidecar write failed (next spawn re-downloads)" >&2
echo "$BINARY_NAME: installed v${DESIRED_VERSION:-unknown} (sha256 + Developer ID verified)" >&2
fi

# Exec-time re-verification: never exec an unverified binary, even one whose
# sidecar version matches (covers binaries installed by pre-hardening wrappers
# and post-install ~/bin tampering). Failure forces one re-download attempt.
if ! verify_binary "$BINARY"; then
if $NEED_DOWNLOAD; then
# We JUST downloaded + verified it; a failure here means tampering
# mid-flight — refuse outright.
echo "$BINARY_NAME: ERROR — freshly installed binary failed re-verification" >&2
exit 1
fi
echo "$BINARY_NAME: existing binary failed signature verification — re-downloading" >&2
rm -f "$BINARY" "$VERSION_FILE"
exec "${BASH_SOURCE[0]}" ${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}
fi

exec "$BINARY" ${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}
Loading
Loading