0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自宅のWindows PCを使って、スマホのClaudeからObsidianのVaultを読み書きする(月額0円)

0
Posted at

出先でスマホのClaudeに設計を相談すると、その場ではいい結論が出ます。でもそれがチャット履歴の中に埋もれて、作業の記録として残らないのが不満でした。

家のPCならClaudeからObsidianのVaultに直接書き込めるので、判断の経緯も決定事項もそのまま蓄積されます。同じことを出先でもやりたい、というのがきっかけです。

帰宅後に履歴を見返して手で書き写す運用も試しましたが、まず続きませんでした。

独自ドメインもVPSも使わず、月額0円で、自宅のWindows PCをそのままサーバーにします。

先に結論(構成)

役割 使うもの 費用
公開 Tailscale Funnel 無料
認証 GitHub OAuth App 無料
サーバー FastMCP(Python) 無料
[スマホのClaude]
      ↓
[Anthropicのクラウド]
      ↓ HTTPS
[Tailscale Funnel]
      ↓
[自宅Windows PC: FastMCPサーバー] → Vaultのフォルダを直接読み書き

最初に理解しておくべき前提

ここを誤解していると延々ハマります。

Claudeのカスタムコネクタは、スマホからではなくAnthropicのクラウドからMCPサーバーに接続します。

つまり、スマホとPCの間にVPN(Tailscaleの通常接続など)を張るだけでは届きません。サーバーをインターネットからHTTPSでアクセスできる状態にする必要があります。

とはいえVaultの中身を素通しで公開するわけにはいかないので、OAuthで認証をかけます。Claudeのカスタムコネクタは「OAuth」か「認証なし」の二択なので、固定トークンをヘッダーに入れる方式は使えません。

なぜTailscale Funnelか

最初はCloudflare Tunnelを検討しました。Tunnel自体もAccessも無料ですが、固定URLを得るには独自ドメインが必要で、年1,500円ほどかかります。

無料のクイックトンネル(trycloudflare.com)はURLが再起動ごとに変わるため、常用できません。

Tailscale Funnelなら、個人プランの無料枠で https://マシン名.tailXXXX.ts.net という固定のHTTPS URLが証明書込みで手に入ります。ドメインの取得も更新も不要です。

環境

  • Windows 11
  • Python 3.12 以上
  • Obsidian(ただし起動していなくても動きます。サーバーはVaultのフォルダを直接読み書きするため)

STEP 1: Tailscale Funnelで公開URLを確定させる

GitHub OAuth Appの設定に公開URLが必要なので、ここから始めます。

1-1. インストールとサインイン

tailscale.com/download からWindows版を入れて、サインインします。GitHubアカウントでそのままサインアップできます。

1-2. MagicDNSとHTTPS証明書を有効化

管理コンソールのDNSページを開きます。

  • tailXXXX.ts.net のようなTailnet nameが表示されます。これが使えるドメインです
  • MagicDNS を Enable
  • 下部の HTTPS Certificates を Enable

1-3. Funnelを起動

PowerShellで実行します。

& "C:\Program Files\Tailscale\tailscale.exe" funnel --bg 8000

初回はブラウザで承認ページが開きます。承認すると証明書の発行とポリシーへの属性追加が自動で行われるので、もう一度同じコマンドを実行してください。

Available on the internet:

https://マシン名.tailXXXX.ts.net/
|-- proxy http://127.0.0.1:8000

この公開URLを控えます。以降 <公開URL> と書きます。

--bg を付けるとバックグラウンド常駐し、設定はTailscale側に永続化されるのでPC再起動後も復活します。この操作は一度きりです。


STEP 2: GitHub OAuth Appを作る

  1. github.com/settings/developers → OAuth Apps → New OAuth App
  2. 入力内容
    • Application name: Obsidian MCP(任意)
    • Homepage URL: <公開URL>
    • Authorization callback URL: <公開URL>/auth/callback ← ここが最重要
  3. Register application
  4. Client ID をコピー
  5. Generate a new client secret で Client Secret をコピー(一度閉じると二度と見られません)

STEP 3: MCPサーバーを書く

py -m pip install fastmcp

C:\mcp\obsidian_mcp_server.py を作成します。

obsidian_mcp_server.py
"""Obsidian Vault MCP server (FastMCP + GitHub OAuth)."""

import os
import sys
from pathlib import Path

from fastmcp import FastMCP
from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.dependencies import get_access_token

VAULT = Path(os.environ["VAULT_PATH"]).resolve()
BASE_URL = os.environ["BASE_URL"]
ALLOWED_USERS = {
    u.strip()
    for u in os.environ.get("ALLOWED_GITHUB_USERS", "").split(",")
    if u.strip()
}

# 未設定だとGitHubアカウントを持つ全員がVaultを読み書きできてしまうため、
# 起動時点で弾く
if not ALLOWED_USERS:
    sys.exit("ALLOWED_GITHUB_USERS が未設定です。接続を許可するGitHubユーザー名を指定してください。")

auth = GitHubProvider(
    client_id=os.environ["GITHUB_CLIENT_ID"],
    client_secret=os.environ["GITHUB_CLIENT_SECRET"],
    base_url=BASE_URL,
    allowed_client_redirect_uris=[
        "https://claude.ai/api/mcp/auth_callback",
        "https://claude.com/api/mcp/auth_callback",
    ],
)

mcp = FastMCP(name="Obsidian Vault", auth=auth)


def _require_user() -> str:
    """許可したGitHubユーザー以外を弾く。"""
    login = (get_access_token().claims or {}).get("login")
    if login not in ALLOWED_USERS:
        raise ValueError(f"このGitHubユーザーは許可されていません: {login}")
    return login


def _resolve(rel_path: str, *, markdown: bool = True) -> Path:
    """Vault外へのアクセスを防ぎつつ絶対パスに変換する。"""
    path = (VAULT / rel_path).resolve()
    if path != VAULT and VAULT not in path.parents:
        raise ValueError(f"Vaultの外は参照できません: {rel_path}")
    # プラグイン設定などを壊さないよう、隠しフォルダは触らせない
    if any(part.startswith(".") for part in path.relative_to(VAULT).parts):
        raise ValueError(f"隠しフォルダは操作できません: {rel_path}")
    if markdown and path.suffix == "":
        path = path.with_suffix(".md")
    return path


@mcp.tool
def vault_list(subdir: str = "") -> list[str]:
    """Vault内のノート一覧を返す。subdirを指定するとその配下のみ。"""
    _require_user()
    root = VAULT if not subdir else _resolve(subdir, markdown=False)
    return sorted(
        p.relative_to(VAULT).as_posix()
        for p in root.rglob("*.md")
        if p.is_file() and not any(x.startswith(".") for x in p.relative_to(VAULT).parts)
    )


@mcp.tool
def vault_search(query: str, limit: int = 20) -> list[dict]:
    """Vault内をテキスト検索し、ヒットしたノートと該当行を返す。"""
    _require_user()
    needle = query.lower()
    results: list[dict] = []
    for path in sorted(VAULT.rglob("*.md")):
        rel = path.relative_to(VAULT)
        if not path.is_file() or any(x.startswith(".") for x in rel.parts):
            continue
        try:
            text = path.read_text(encoding="utf-8")
        except (UnicodeDecodeError, OSError):
            continue
        if needle not in text.lower():
            continue
        lines = [line.strip() for line in text.splitlines() if needle in line.lower()][:3]
        results.append({"path": rel.as_posix(), "matches": lines})
        if len(results) >= limit:
            break
    return results


@mcp.tool
def vault_read(path: str) -> str:
    """ノートの本文を読む。pathはVaultルートからの相対パス。"""
    _require_user()
    return _resolve(path).read_text(encoding="utf-8")


@mcp.tool
def vault_write(path: str, content: str) -> str:
    """ノートを新規作成または上書きする。フォルダは自動で作成される。"""
    _require_user()
    target = _resolve(path)
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content, encoding="utf-8")
    return f"wrote: {target.relative_to(VAULT).as_posix()}"


@mcp.tool
def vault_patch(path: str, content: str, mode: str = "append") -> str:
    """既存ノートに追記する。modeはappend(末尾)かprepend(先頭)。"""
    _require_user()
    target = _resolve(path)
    if not target.exists():
        raise ValueError(f"ノートが存在しません: {path}")
    original = target.read_text(encoding="utf-8")
    if mode == "append":
        updated = original.rstrip("\n") + "\n\n" + content.strip() + "\n"
    elif mode == "prepend":
        updated = content.strip() + "\n\n" + original.lstrip("\n")
    else:
        raise ValueError("modeはappendかprependを指定してください")
    target.write_text(updated, encoding="utf-8")
    return f"patched ({mode}): {target.relative_to(VAULT).as_posix()}"


if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8000)

起動用バッチ

C:\mcp\start-mcp.bat を作ります。

start-mcp.bat
@echo off
set "VAULT_PATH=C:\Users\xxxx\Documents\MyVault"
set "BASE_URL=https://マシン名.tailXXXX.ts.net"
set "GITHUB_CLIENT_ID=取得したClient ID"
set "GITHUB_CLIENT_SECRET=取得したClient Secret"
set "ALLOWED_GITHUB_USERS=あなたのGitHubユーザー名"
py "C:\mcp\obsidian_mcp_server.py"

BASE_URL にポート番号を付けないでください。Funnelは公開443をローカル8000へ転送するので、外から見えるURLにポートは含まれません。ここが食い違うとOAuthが必ず失敗します。

疎通確認は401が正解

バッチを実行し、ブラウザで <公開URL>/mcp を開きます。

HTTP/1.1 401 Unauthorized
www-authenticate: Bearer scope="user", resource_metadata="..."

401が返れば成功です。 外部から到達でき、かつ認証で保護されていることが同時に確認できます。


STEP 4: Claudeにコネクタを登録する

スマホアプリからは登録できないことがあるので、PCのブラウザで claude.ai を開きます。

  1. 設定 → コネクタ → カスタムコネクタを追加
  2. 名前: Obsidian、URL: <公開URL>/mcp
  3. 「接続」→ GitHubの認可画面で Authorize

Web側で登録すればスマホアプリにも同期されます。スマホのClaudeを開き直して「Vaultのノート一覧を見せて」と話しかければ動作確認できます。


STEP 5: 自動起動

タスクスケジューラに登録します。黒いコンソールを出したくないので、C:\mcp\start-mcp.vbs を経由させます。

start-mcp.vbs
CreateObject("WScript.Shell").Run """C:\mcp\start-mcp.bat""", 0, False

「タスクの作成」から以下を設定します。

  • トリガー: ログオン時 / 遅延 30秒(Tailscaleの起動を待つため)
  • 操作: プログラムの開始 → C:\mcp\start-mcp.vbs
  • 条件: 「AC電源で使用している場合のみ」のチェックを外す
  • 設定: 「タスクを停止するまでの時間」のチェックを外す(既定の3日で止まるのを防ぐ)

あわせて、電源設定でスリープを「なし」にしておきます。スリープ中はアクセスできません。


ハマりどころ

1. python がMicrosoft Storeのダミーに横取りされる

PS> where.exe python
C:\Users\xxxx\AppData\Local\Microsoft\WindowsApps\python.exe   ← ダミー
C:\Users\xxxx\AppData\Local\Python\bin\python.exe              ← 本物

WindowsApps 配下のものはStoreへ誘導するだけのスタブです。py コマンドを使うか、設定 → アプリ → アプリ実行エイリアス で python.exe と python3.exe をオフにします。

2. PCから公開URLをcurlしても、外部到達の確認にならない

これが一番ややこしい罠でした。

curl.exe -i https://マシン名.tailXXXX.ts.net/mcp
# → 401が返る。でもこれは外部到達の証明にならない

MagicDNSが *.ts.net をTailnet内のIP(100.x.x.x)に解決するため、Funnelの中継を通らず自分自身に直接繋がっています。

外部到達を確認するには、スマホのWi-FiとTailscaleアプリを両方オフにして、モバイル回線からブラウザでアクセスしてください。

3. Test-NetConnection の True を信用しない

TcpTestSucceeded : True

これはTCPハンドシェイクが成立しただけで、HTTPの応答までは見ていません。プロセスがハングしていてもTrueになります。HTTPレベルの確認には curl.exe -i -m 10 http://127.0.0.1:8000/mcp を使ってください。

4. 突然繋がらなくなったら、まずFunnelを張り直す

プロセスもFunnelも生きているのに、Anthropic側から届かなくなることが稀にあります。中継経路が壊れた状態です。

& "C:\Program Files\Tailscale\tailscale.exe" serve reset
& "C:\Program Files\Tailscale\tailscale.exe" funnel --bg 8000

URLは変わらないので、GitHubやClaude側の再設定は不要です。 自分の場合はこれで復旧しました。

5. 公開するとボットのスキャンが常時来る

"GET /.env HTTP/1.1" 404 Not Found
"GET /wp-config.php HTTP/1.1" 404 Not Found
"GET /.ssh/id_rsa HTTP/1.1" 404 Not Found

インターネットに公開すれば必ず来るもので、狙われたわけではありません。このサーバーは静的ファイルを配信しておらず、Vaultの中身は認証済みのツール呼び出しでしか触れないため、すべて404で弾かれます。

ログを静かにしたいなら、Funnelの公開ポートを443以外(--https=8443)に変えるとスキャンはほぼ止まります。ただしURLが変わるので、OAuthのコールバックとコネクタ設定をすべて直す必要があります。


セキュリティ面の注意

ALLOWED_GITHUB_USERS は必ず設定してください。これが無いと、GitHubアカウントを持つ誰でもあなたのVaultを読み書きできます。上のコードは未設定だと起動しないようにしてあります。

そのうえで、インターネットに自宅PCのポートを開けている事実は変わりません。Vaultに業務の機密情報や個人情報を入れている場合は、この構成を使うかどうか慎重に判断してください。

Tailnet名とマシン名の組み合わせが、そのまま公開URLになります。 https://マシン名.tailXXXX.ts.net という形式なので、この2つが揃って漏れると第三者があなたのエンドポイントを特定できます(認証は突破されませんが、攻撃対象にはなります)。

ブログやSNSに手順を書く際、Tailscaleの管理画面やタスクスケジューラのスクリーンショットには注意してください。マシン名やWindowsのユーザー名が写り込みがちです。


使ってみた感想

一番変わったのは、出先での設計作業が使い捨てにならなくなったことです。

これまでは移動中にスマホのClaudeと設計を詰めても、結論がチャット履歴の中に留まったままでした。今は作業の区切りでそのままVaultに書かせられるので、「なぜこの構成にしたか」まで含めて残ります。次に同じ話題を開いたとき、Claudeがその記録を読んだ状態から始められるのも大きいです。

ファイル名もフォルダも指定しなくていい(既存のノートを検索して適切な場所に追記してくれる)ので、PCの前にいるときと同じ感覚で使えます。

既存のOSSにもykoellmann/obsidian-mcpやnweii/obsidian-remote-mcpといった完成度の高いものがありますが、いずれもサーバーやNASにVaultを同期して置く前提です。手元のWindows PCをそのまま使いたい場合は、この記事の構成が最短だと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?