obsidian-mcp: 初次落地 Obsidian Vault MCP Server (.NET 10, read+write)
把 Obsidian vault 通过 MCP 暴露给 Claude.ai,OAuth 走 nas-auth。 设计文档见 vault Coding/obsidian-mcp/obsidian-mcp 设计.md。 代码层落地参考 vault Coding/obsidian-mcp/MCP 实现指南.md。 V1+V2 同时实现(用户要求跳过分阶段直接全部): 读 Tools(需 scope=read:obsidian): - list_vault_tree(一次性 vault 地图,限制深度) - list_files / read_file(含 offset/limit 大文件分页) - search(子串匹配 + glob 过滤,最多 50 hits) - get_metadata(size / modified_at / has_frontmatter) 写 Tools(需 scope=write:obsidian): - write_file / append_file - 多重门禁:scope 校验 + 路径黑名单 + 写入白名单 + 永禁文件 - 永禁写:任意目录的 AGENTS.md / PROFILE.md / README.md / CLAUDE.md / 01-Secret/** - 白名单:02-ShengquGames/logs/ + Coding/ + NAS/NAS 待办清单.md - 写入审计日志按天 rotate(JSON line) 安全: - VaultPathResolver chroot:path traversal + symlink 双拒绝 - JwtBearer (HS256, Current+Previous fallback, MapInboundClaims=false) - aud=obsidian, iss=https://auth.zhengchentao.win - 黑名单:01-Secret / .obsidian / .trash / .git 技术栈: - .NET 10 + ModelContextProtocol SDK 1.0 - Streamable HTTP transport (POST /mcp) - JwtBearer 10.0 + IdentityModel.Tokens 8.x 部署: - Dockerfile multi-stage,runtime 装 ripgrep(V3 备用),non-root user - .gitea/workflows/build-image.yml:build + deploy 双 job,buildkit v0.13.2 - 容器内 :8080,宿主端口 9090 - 子域名 obs.zhengchentao.win - vault 挂载 /volume1/docker/webdav/data/Zhengchen:/vault:rw(V2 写入需要 rw) 测试:35/35 单测过(VaultPathResolver path traversal/blacklist/symlink + VaultWriteGuard whitelist/forbidden) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
# 自动触发:push 到 main 分支(纯文档改动跳过)
|
||||
# ⚠️ quirk:git commit --allow-empty 不触发 paths-ignore 过滤的 workflow,
|
||||
# 要强制重新触发必须改一个非 ignore 路径的真实文件(改 build-image.yml 自己最稳)
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '.gitignore'
|
||||
- '.dockerignore'
|
||||
# 手动触发:应急通道(重新打包 / 指定自定义 tag)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: '要打包的分支(仅手动触发生效)'
|
||||
required: true
|
||||
default: 'main'
|
||||
tag:
|
||||
description: '镜像 tag(留空则用 commit short hash)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
# 同分支连续 push 只跑最新的 run,旧 in-progress run 被取消(build + deploy 一起停)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout target branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# 钉 v0.13.2(runc 1.1.x)避免 DSM 4.4.x 内核不支持 runc 1.2+ 的
|
||||
# openat2/fsmount syscall 导致 build 失败
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.13.2
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.zhengchentao.win
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PACKAGES_TOKEN }}
|
||||
|
||||
- name: Determine image tag and revision
|
||||
id: meta
|
||||
run: |
|
||||
if [ -n "${{ inputs.tag }}" ]; then
|
||||
IMAGE_TAG="${{ inputs.tag }}"
|
||||
else
|
||||
IMAGE_TAG="$(git rev-parse --short HEAD)"
|
||||
fi
|
||||
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "full_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "==> Image tag: $IMAGE_TAG"
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://git.zhengchentao.win/zhengchen.tao/obsidian-mcp
|
||||
org.opencontainers.image.revision=${{ steps.meta.outputs.full_sha }}
|
||||
tags: |
|
||||
git.zhengchentao.win/zhengchen.tao/obsidian-mcp:${{ steps.meta.outputs.image_tag }}
|
||||
git.zhengchentao.win/zhengchen.tao/obsidian-mcp:latest
|
||||
|
||||
- name: Build summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## Build Summary"
|
||||
echo ""
|
||||
echo "| 项 | 值 |"
|
||||
echo "|---|---|"
|
||||
echo "| 触发方式 | \`${{ github.event_name }}\` |"
|
||||
echo "| 源分支 | \`${{ inputs.branch || github.ref_name }}\` |"
|
||||
echo "| 源 commit (full) | \`${{ steps.meta.outputs.full_sha }}\` |"
|
||||
echo "| 源 commit (short) | \`${{ steps.meta.outputs.image_tag }}\` |"
|
||||
echo "| 镜像 | \`git.zhengchentao.win/zhengchen.tao/obsidian-mcp:${{ steps.meta.outputs.image_tag }}\` + \`:latest\` |"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
deploy:
|
||||
# needs: build 串起来 —— build 失败则 deploy 自动 skip,无需 if 条件
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# deploy job 跑在独立 runner 容器上,凭据不从 build job 继承,必须再登一次
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.zhengchentao.win
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PACKAGES_TOKEN }}
|
||||
|
||||
- name: Pull and restart obsidian-mcp
|
||||
env:
|
||||
NAS_INFRA_TOKEN: ${{ secrets.NAS_INFRA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
if [ -n "$NAS_INFRA_TOKEN" ]; then
|
||||
CLONE_URL="https://x-access-token:${NAS_INFRA_TOKEN}@git.zhengchentao.win/dev/nas-infra.git"
|
||||
else
|
||||
CLONE_URL="https://git.zhengchentao.win/dev/nas-infra.git"
|
||||
fi
|
||||
|
||||
git clone --depth 1 "$CLONE_URL" "$TMPDIR/nas-infra"
|
||||
cd "$TMPDIR/nas-infra/obsidian-mcp"
|
||||
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
sleep 3
|
||||
docker compose ps
|
||||
docker compose logs --tail=30 obsidian-mcp
|
||||
Reference in New Issue
Block a user