pse-edge-mcp について
pse-edge-mcpは、フィリピン株式市場を調査する人や金融アプリ向けに、Philippine Stock ExchangeとBangko Sentral ng Pilipinasのデータを提供します。株価、開示情報、財務、金利を扱い、データ種別に応じたキャッシュと価格凍結の方針を備えています。
主な機能
- 01株価と価格履歴の取得
- 02開示情報の検索と添付リソースの読み取り
- 03企業プロフィール・財務・配当の取得
- 04指数・市場概要・BSP金利の確認
- 05データ種別ごとのキャッシュと古さの表示
活用シーン
- 01上場企業の価格と財務の調査
- 02企業開示情報と添付ファイルの検索
- 03市場指数と中央銀行の主要金利の確認
MCP 対応
MCPクライアントに、読み取り専用の金融データツール、添付リソース、2つのプロンプトを公開します。認証を有効にした環境では、認証ユーザー本人にのみメールを送るアクションツールも利用できます。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://pse.sakayandgo.com/mcpclaude mcp add --transport http phdwight-pse-edge https://pse.sakayandgo.com/mcp{"mcpServers":{"phdwight-pse-edge":{"type":"http","url":"https://pse.sakayandgo.com/mcp"}}}Cursor で開く{"servers":{"phdwight-pse-edge":{"type":"http","url":"https://pse.sakayandgo.com/mcp"}}}VS Code で開く{"mcpServers":{"phdwight-pse-edge":{"type":"http","url":"https://pse.sakayandgo.com/mcp"}}}{"mcpServers":{"phdwight-pse-edge":{"serverUrl":"https://pse.sakayandgo.com/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
詳細
pse-edge-mcp
PSE Edge ポータルのデータ(株価、価格履歴、開示情報、財務報告、市場データ)に加え、Bangko Sentral ng Pilipinas(中央銀行)の主要金利を Claude やその他の MCP クライアントに提供する、Philippine Stock Exchange の MCP サーバーです。
非公式です。 PSE Edge と BSP には公開 API がないため、このプロジェクトは各サイトのページが利用するものと同じエンドポイントにアクセスします。PSE または BSP との提携や、いずれかからの推奨を受けているものではありません。データは個人利用および調査目的で、保証なしに現状のまま提供されます。
目次
- 機能
- クイックスタート
- 設計:終値は日次、それ以外は一度だけ取得
- ツール、リソース、プロンプト
- アーキテクチャ
- 基本的な流れ:1 件のリクエストを追跡
- ホストされたサーバーへの接続
- Docker Compose で実行
- 設定
- コンテナイメージ
- 本番環境
- 開発
- コントリビューション
- ライセンス
機能
- 株価、価格履歴、開示情報(メタデータ、全文、詳細)、企業プロフィール、財務情報、配当、指数、市場サマリー、BSP(中央銀行)の主要金利を網羅する、15 個の読み取りツールと 1 個のアクションツールを提供します。シンボル補完付きの添付ファイル用リソース 1 個、プロンプト 2 個、ツールのアノテーションも備えています。
- PSE Edge への負荷を意図的に抑えます。 個別のクエリは 1 日に最大 1 回だけ取得し、価格は厳格な市場境界で凍結されます。
- 人間向けの OAuth 2.1 とパスキー(パスワードは一切不要)と、ヘッドレスエージェント向けの
client_credentialsに対応しています。どちらもオプトイン方式です。stdio では何も必要ありません。 - Postgres は任意です。 ローカルの stdio では設定不要のインメモリ構成を使用でき、
DATABASE_URLを設定すると共有キャッシュと継続的に拡充される EOD アーカイブを利用できます。 - 変更を見逃しません。 夜間のカナリアが実際のモデルを使ってライブページを検証し、失敗時にのみアラートを送信します。ページのスタイルが変更された場合はエラーになり、不完全なデータが返されることはありません。
- マルチアーキテクチャ対応コンテナイメージ(amd64 + arm64)を必要性に基づくチェック付きで提供します。イメージには実行時依存関係のみが含まれ、それ以外は含まれません。
クイックスタート
Claude Desktop / Claude Code(stdio):
uvx pse-edge-mcp
{
"mcpServers": {
"pse-edge": { "command": "uvx", "args": ["pse-edge-mcp"] }
}
}
ホスト型デプロイ(認証有効)は、標準的な OAuth 2.1 保護リソースです。最新のクライアントは URL だけで利用でき、フロー全体を自動で処理します(詳細はこちら)。
{
"mcpServers": {
"pse-edge": { "url": "https://your-host.example.com/mcp" }
}
}
Docker(HTTP + Postgres):
cp .env.example .env # set POSTGRES_PASSWORD
docker compose up --build
設計:終値は日次、それ以外は初回取得
依存関係が一方向に並ぶ4つのレイヤーです。すべての読み取りは、ドメインごとのポリシーを明示して FreezeService.get() を経由します。HTTP クライアントに直接アクセスするツールはありません。★は、この設計が守るべき不変条件を示し、価格のみを対象とします。
flowchart TD
C["MCP client"] -- "tool call" --> S["<b>server.py</b> · MCP boundary<br/>validate args · delegate · shape reply"]
S --> R["<b>repositories.py</b> · one per domain<br/>cache key · freeze read · parse · endpoint routing"]
R --> F["<b>service.py</b> · FreezeService ★<br/>3 policies · miss → fetch once"]
F --> P["<b>client.py</b> · PseEdgeClient<br/>throttled HTTPS · single-flight · 2 dialects"]
P --> E["PSE Edge<br/>edge.pse.com.ph"]
★ 市場時間中の価格凍結 — 価格のみ。 キャッシュ済みの株価は、市場が開いている間(取引日の 09:30~15:00、Asia/Manila)に再取得されません。前回の終値が返され、stale としてフラグ付けされます。これまで一度もリクエストされたことのない価格のみが例外で、セッション中に一度だけ取得され、銘柄識別情報と previous_close のみが返されます(セッション中に変動するすべてのフィールドは除外されます)。また、stale: true と、リアルタイム値ではないことを示す meta.note が付与されます。確定した数値は取引終了後に置き換わります。
| ポリシー | 対象 | 動作 |
|---|---|---|
EOD-frozen ★ | get_stock_quote、get_price_history | キャッシュ済みの価格はセッション中に再取得されません。キャッシュされたことのないキーは一度だけ取得され、セッション全体を通して、リアルタイム値ではないことを明記したうえで previous_close のみが返されます。またデフォルトポリシーでもあるため、ポリシーが明示されていない読み取りで PSE Edge を過剰に保護することはあっても、保護が不足することはありません。 |
daily-refresh | 企業情報、開示情報、プロフィール、財務情報、配当、指数、サマリー、BSP レート | 最初のリクエストでは、時間を問わず取得します。同時に呼び出された場合は重複を排除します。同じクエリへの以降のリクエストには、次の 15:00 の取引終了までストレージから応答します。 |
immutable | edge_no による開示情報の詳細、添付ファイルのバイトデータ | 上流で変更されないオブジェクトです。これまでに一度だけ取得され、valid_until: null となります。 |
PSE Edge に接続できず、期限切れのエントリーが存在する場合、ツールは実データをエラーで破棄せず、meta.stale: true として返します。EDGE_UNAVAILABLE は、接続不能で、かつキャッシュも存在しないことを意味します。
すべてのデータツールは同じエンベロープを返します。鮮度に関する契約は meta に記述されます。
{
"data": { /* …StockQuote… */ },
"meta": {
"as_of": "2026-08-06T15:00:00+08:00", // ISO-8601, Asia/Manila
"valid_until": "2026-08-07T15:00:00+08:00", // null when immutable
"from_cache": false,
"stale": false, // true = not a settled EOD value
"data_policy": "EOD-frozen", // "daily-refresh" / "immutable" elsewhere
"note": null // freshness caveat, e.g. "not a realtime value"
}
}
ツール、リソース、プロンプト
| ツール | 説明 |
|---|---|
search_companies(query) | 名前またはティッカーでPSE上場企業を検索します |
validate_symbol(symbol) | ティッカーの存在を簡単なはい/いいえで確認し、企業名とIDを返します |
get_stock_quote(symbol) | 最新のEOD株価情報:価格、変動、52週間の値幅、時価総額、全項目 |
get_price_history(symbol, start_date?, end_date?) | Edgeのチャートエンドポイントから日次OHLC系列を取得します |
search_disclosures(symbol?, start_date?, end_date?, template?, page?) | 市場全体または企業ごとの開示メタデータ。正確な合計件数とともに1ページあたり50件を返します |
search_disclosure_fulltext(keyword, ...) | 開示添付ファイル内のテキストをスニペット付きで検索します |
get_disclosure(edge_no, max_files?) | 1件の開示の詳細と、添付ファイルおよび本文HTMLへのリンクを返します。添付ファイル数はmax_files(デフォルト20)までで、切り捨ての有無を正確に示します。各添付ファイルにはresource_uriが含まれます。MCPのresources/readを使ってファイルのバイトデータを読み取ってください(pse-edge://attachment/<file_id>、不変キャッシュ、上限10 MB) |
get_company_profile(symbol) | セクター、設立、監査人、名義書換代理人、連絡先 |
get_financial_highlights(symbol) | 年次および四半期の貸借対照表と損益計算書 |
get_dividends_and_rights(symbol) | 開示へのリンク付きの配当宣言と株式権利 |
get_indices() | PSEiと7つのセクター指数、および符号付きの日次変動 |
get_market_summary() | 指数水準とPSE Edgeのホームページにある開示フィード |
get_bsp_key_rates() | Bangko Sentral ng Pilipinas(中央銀行)の主要金利ダッシュボード:政策金利、ファシリティ金利、入札金利、総合インフレ率、USD/ペソ基準レート |
get_bsp_policy_rate() | BSPの政策金利(Target RRP)と、その周辺の翌日物貸出・預金金利コリドー |
get_server_version() | このMCPサーバー自体のデプロイ済みバージョン(/healthと一致します) |
send_email(subject, body) | メモを自分宛てにメール送信します(認証が有効なデプロイのみ) |
ツールに加えて、サーバーは上記の添付ファイルリソース、2つのプロンプト(market_recap、company_briefing(symbol) — symbol引数はPSE Edge独自の検索結果から自動補完されます)、およびホストが読み取り専用ツールを自動承認できるようにするMCPのツールアノテーションを提供します。MCP Registry向けの説明はserver.jsonに記載されています。
send_emailは、読み取りではなく操作を行う唯一のツールです。宛先引数はありません。メッセージは常にセッションの認証に使われたアカウントに送信されるため、中継には使用できず、プロンプトインジェクションによって転送先を変更することもできません。この点は、オペレーターが管理できない開示テキストをこのサーバーが返すため重要です。認証が有効なデプロイでのみ表示され(それ以外では確認済みのメールアドレスがありません)、本文はHTMLとしてレンダリングされずエスケープされ、ユーザーあたり1日20通までに制限されています。
開示ツールが返すのはメタデータとリンクのみです。このサーバーが添付ファイルをダウンロードまたは解析することはありません(明示的なリソース読み取りを除く)。ファイルが必要な場合は、MCPクライアントから返されたURLを直接取得できます。Edge独自の全文検索インデックスは一部のデータに限られているため(最終確認時点ではおおむね2023~2025年)、search_disclosure_fulltextはsearch_disclosuresの代替にはなりません。この制限は検索結果にも表示されます。
財務数値はPSE Edgeの表示どおりに返され、換算されることはありません。Edge独自の単位ラベルは年次と四半期のセクション間で一貫していないため、各期間にcurrency_unitsが含まれており、確認できます。Edgeは指数の変動を色と矢印で方向のみ示し、符号を付けずに表示しますが、ここでは変動に符号が付いています。
アーキテクチャ
レイヤー
| レイヤー | 担当するもの | 決して行わないもの |
|---|---|---|
server.py — MCP 境界 | 引数の検証、委譲、応答の整形。エラーのマッピングは reply() で一度だけ行います。アクションツールは act() を通ります(鮮度を示す meta はありません)。 | ドメインロジック、キャッシュキー、解析、エンドポイントの選択 |
repositories.py — ドメイン層 | データドメインごとにリポジトリを1つ設けます。キャッシュキー、フリーズ読み取り、解析、Pydantic モデルを担当します。エンドポイントのルーティングはここにあります。 | 具象クライアントへの依存 — 以下のプロトコルのみに依存します |
service.py / sources.py — ポリシーと接点 | FreezeService が読み取りごとのポリシーを適用し、すべての結果を Served[T](値 + as_of、valid_until、from_cache、stale)でラップします。sources.py はドメインごとに絞った5つのソースプロトコルを宣言します。FrozenCache はキャッシュの接点です。 | — |
client.py / parsers.py — 外界との境界 | 純粋な HTTP で、MCP には依存しません。トークンバケット方式のスロットリング、シングルフライト、再試行を行います。2種類のリクエスト形式(チャートの .ax エンドポイント向け JSON 本文の POST、search.ax 向けフォームエンコード POST による HTML フラグメントの返却)に対応します。パーサーは HTML/JSON を dict に変換します。形式のずれがあれば EndpointChangedError を発生させます — 明確に失敗し、不完全な結果は返しません。 | — |
コアクラス一覧
5つのリポジトリで、ツール全体を網羅します。それぞれが絞り込まれたソースプロトコルを使用します。具象クライアントは5つすべてを満たしますが、各リポジトリはそのことを認識しないため、数行のフェイクを用意するだけで HTTP のモックなしにテストできます。
| リポジトリ | メソッド → モデル | 使用するもの | ポリシー/備考 |
|---|---|---|---|
CompanyRepository | search、resolve、try_resolve → CompanyHit | CompanySource | daily-refresh · 他のすべてのリポジトリ向けにシンボルを company_id に解決します |
QuoteRepository | quote → StockQuote · history → PriceHistory | QuoteSource | EOD-frozen ★ — 市場の制約を受ける唯一のドメイン · バーをアーカイブします |
DisclosureRepository | search、fulltext、detail、attachment | DisclosureSource | 検索は daily-refresh、detail/attachment は immutable |
CompanyInfoRepository | profile、financials、dividends_and_rights | CompanyInfoSource | daily-refresh · 財務単位は加工せずそのまま渡します |
MarketRepository | indices、summary | MarketSource | daily-refresh · 指数の符号は Edge の ▲/▼ グリフから導出します |
NotificationService (アクション) | send(user, subject, body) → SentEmail | — | 受信者はベアラートークンから取得し、引数では受け取りません |
プロトコルと差し替え可能な実装
切り替えは1つの環境変数で行います。DATABASE_URL が未設定ならインメモリ/Null、設定されていれば Postgres を使います。Postgres モジュールは遅延インポートされるため、軽量インストールでは不要な負荷がかかりません。
| プロトコル | DATABASE_URL 未設定 | DATABASE_URL 設定済み |
|---|---|---|
Storage | InMemoryStorage | PostgresStorage |
Archive | NullArchive | PostgresArchive |
UsageSink | NullUsageRecorder | PostgresUsageSink |
AuthStore | — | PostgresAuthStore |
EmailSender | ConsoleEmailSender | ZEPTOMAIL_API_KEY が設定されている場合は ZeptoMailSender |
HTTP の組み立て — asgi.py で一度だけ行います
flowchart LR
H["HealthApp<br/>/health · /health/ready"] --> A["AuthApp<br/>/oauth/* · signup · /account · /privacy"]
A --> M["AuthMiddleware<br/>bearer validation · quotas · usage"]
M --> MCP["MCP app<br/>the tool surface"]
/health は稼働状態を確認するエンドポイントで、データベースには一切アクセスしません。/health/ready は準備状態を確認します。AuthApp の背後には、OAuthService(DCR · PKCE のみ · リフレッシュファミリー)、PasskeyService(WebAuthn + ウェブセッション)、TokenService(不透明な pse_ トークン、保存時に SHA-256 を適用)があります。
エラー体系 — ルートは1つ、reply() で一度だけマッピング
| エラー | 意味 |
|---|---|
SymbolNotFoundError | SYMBOL_NOT_FOUND |
InvalidArgumentError | INVALID_ARGUMENT — 不正な入力。境界で捕捉します |
EndpointChangedError | Edge がページを再設計しました — 明確に失敗し、不完全な結果は返しません |
EdgeUnavailableError | 上流に接続できず、かつキャッシュにも何もありません |
MarketOpenNoCacheError | クライアントとの互換性維持のため残しています。現在は発生しません |
ActionUnavailableError | アクションツールには認証の有効化が必要です |
ActionRateLimitedError | ユーザーあたり1日20件のメール |
ウォッチドッグ
毎晩実行されるカナリア(pse-edge-canary と Compose サービス)は、キャッシュを迂回して実際のページを取得し、リポジトリが構築するものと同じ Pydantic モデルで検証します。スタイル変更されたテーブルを含む 200 レスポンスこそ、これが検出するための障害です。市場が開いている間は引き続き実行を拒否し(★の不変条件が優先されます)、PSE_OPERATOR_EMAIL には失敗時のみメールを送り、cron が検知できるようゼロ以外の終了コードを返します。
ゴールデンパス:1 回のリクエストを追跡する
市場終了後、キャッシュが空の状態での get_stock_quote("SM"):
server.py—validation.pyがシンボルの形式を確認します(不正な入力 →INVALID_ARGUMENT)。続いてreply()がリポジトリ呼び出しをラップします。エラーが MCP エラーペイロードになるのは、ここだけです。QuoteRepository.quote("SM")—CompanyRepositoryを通じてSMをcompany_idに解決し、エンドポイントを選択してキャッシュキーを構築します。ツールがこれらを見ることはありません。FreezeService.get(key, fetch, policy="EOD-frozen")★ — キャッシュが新しければ、その値を返します。市場が開いていてキャッシュがある場合は、最後の終値をstaleとして返し、再取得はしません。市場が開いていてキャッシュがない場合は 1 回だけ取得し、セッション全体を通してstale: trueとnoteを付けます。市場が閉じていてキャッシュがない場合は取得します。取得に失敗して期限切れのエントリーがある場合は、staleとしてその値を返します。PseEdgeClient.fetch_stock_data_page(company_id)— トークンバケット(1 req/s)、シングルフライトの重複排除、再試行を行います。ワイヤー上の日付形式はMM-dd-yyyyです。JSON 形式とフォーム形式のどちらを使うかは、エンドポイントごとに選択されます。parsers.py→StockQuote— HTML → dict → 検証済み Pydantic モデルの順に処理します。Edge のマークアップに変更があると、EndpointChangedErrorが発生します。cache.py/archive.py— エントリーは次の 15:00 の終値まで固定されます。日足バーは機会に応じてアーカイブされます(データベースが停止していても、読み取りは失敗しません)。
ホスト型サーバーへの接続
認証が有効なデプロイは、通常の OAuth 2.1 保護リソースです。そのため、最新の MCP クライアントに必要なのは URL だけです。クライアントがその他の情報をすべて検出し、フロー全体を自動で進めます。
初回接続時の動作
太字で示したブラウザーでの 2 つの手順を除き、手動で行うことはありません。
- クライアントはトークンなしで
/mcpにPOSTし、WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"を含む 401 を受け取ります。このヘッダーがブートストラップ情報のすべてです。次にどこを参照すればよいかをクライアントに伝えます。 - クライアントはそのドキュメントを取得し、このリソースを保護する認可サーバーを確認してから、エンドポイントを取得するために
/.well-known/oauth-authorization-serverを読み込みます。 - クライアントは
/oauth/register(RFC 7591)で自身を登録します。クライアントシークレットも、オペレーターの関与も、事前共有認証情報も必要ありません。client_idが返されます。 - PKCE チャレンジ(S256 が必須)を付けて、ブラウザーで
/oauth/authorizeを開きます。 - ユーザーが登録またはサインインします。 新規ユーザーは
/signupに移動し、(意図的に最小限にした)データポリシーに同意してメールアドレスを入力すると、リンクを受け取ります。そのリンクを開くと確認ページが表示され、ボタンを押すと/enrollでパスキーが登録されます。この確認手順は、メールスキャナーによる事前取得でリンクが使用されてしまうのを防ぐためにあります。再訪ユーザーは/loginに進み、すでに持っているパスキーを使用します。システム内のどこにもパスワードはありません。 - ユーザーは、クライアント名が表示された同意画面でクライアントを承認します。
- ブラウザーは 1 回限りのコードをクライアントに返します。クライアントは PKCE 検証子を使って
/oauth/tokenでコードを交換し、アクセス トークン(15 min)と 1 回限りのリフレッシュ トークン(24 h)を受け取ります。 - クライアントは
Authorization: Bearer …を付けて/mcpを呼び出し、以降はバックグラウンドでトークンを更新します。ユーザーに再度確認を求めることはありません。
client ──POST /mcp──────────────▶ 401 + WWW-Authenticate
──GET /.well-known/… ───▶ metadata
──POST /oauth/register ──▶ client_id
──GET /oauth/authorize ─▶ browser: signup/login → passkey → consent
◀───────────────────────── ?code=…
──POST /oauth/token ─────▶ access (15 min) + refresh (24 h)
──POST /mcp + Bearer ────▶ tools
リフレッシュトークンは使用するたびにローテーションされます。ローテーション済みのトークンを再使用すると、そのセッションファミリー全体が失効します(RFC 9700 §4.14)。盗まれたリフレッシュトークンは、盗難が検知されてセッションが終了するまでに 1 回だけ使用できます。
ヘッドレスエージェント(client_credentials)
LangGraph アプリ、Anthropic Messages API MCP コネクター、またはブラウザーを開けないエージェント向けです。リダイレクトもパスキーも同意画面も不要で、クライアント ID とシークレットだけで利用できます。
1. プロビジョニングします。 方法は 2 つありますが、結果は同じです。
- Web から(シェルは不要で、NAS で実用的な方法です):アカウントのメールアドレスを
PSE_ADMIN_EMAILSに設定してサインインすると、/accountに作成と取り消しの操作ができる マシンクライアント パネルが表示されます。アクセスは許可リストで制限されており、通常のサインアップでは表示されません。 - CLI から:
pse-edge-admin create-machine-client --name langgraph-app。
どちらの方法でも、client_id と client_secret が表示されるのは 一度だけ です。保存されるのはシークレットの SHA-256 ハッシュのみなので、復元はできません。取り消して再発行することのみ可能です(同じアカウントページから、または pse-edge-admin revoke-machine-client <client_id> を実行します)。
2. トークンを発行します。
curl -s -X POST https://pse.sakayandgo.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET \
-d scope=mcp -d resource=https://pse.sakayandgo.com/mcp
{"access_token": "pse_…", "token_type": "Bearer", "expires_in": 3600, "scope": "mcp"}
HTTP Basic も使えます(curl -u "$CLIENT_ID:$CLIENT_SECRET")。ほとんどの SDK が送信する方式です。リフレッシュトークンは発行されません。クライアントはすでに有効期間の長いシークレットを保持しているため、1 時間が経過したら再リクエストするだけです。
3. 使用します。
curl -s -X POST https://pse.sakayandgo.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
pse-edge-admin revoke-machine-client <client_id> で取り消すと、シークレット、そのシークレットから発行されたすべてのトークン、対応するサービスアカウントが一度に無効化されます。
登録しても、この権限は付与されません。
/oauth/registerはインターネットに公開されているため、自分で登録したクライアントは、grant_types: ["client_credentials"]を宣言してシークレットを送信した場合でも、unauthorized_clientで拒否されます。認可の根拠となるのはclient_typeカラムだけです。このカラムを書き込めるのは管理者 CLI のみであり、登録者が自分について申告した内容は根拠になりません。
エージェントごとに 独自のマシンクライアント を用意してください。クォータはクライアントごとに設定されるため、暴走したジョブは自身のクォータで制限され、1 つを取り消しても他のクライアントには影響しません。
この上にアプリを構築しますか? examples/langgraph_client.py はマルチテナント向けの動作するクライアントです。アプリは 1 つのマシンクライアントで 自身 として認証し、ユーザーがこのサーバーを見ることはありません。1 時間有効なトークンを発行・更新する httpx.Auth(確認済み:同時呼び出しでも発行は 1 回だけ。古くなったトークンも 401 を受けて復旧します)と、システムプロンプトに貼り付けて使えるエージェント向けの指示が含まれています。mcp<2 が必要です。langchain-mcp-adapters は、現時点では 2.x SDK に対してまだインポートできません。
クライアントがまだ OAuth に対応していない場合
オペレーターがトークンを直接発行し、ユーザーがそれをヘッダーに貼り付けます。同じサーバーをブラウザーなしで利用できます。
pse-edge-admin create-user [email protected]
pse-edge-admin issue-token [email protected] --note laptop # plaintext shown once
curl -X POST https://your-host.example.com/mcp \
-H "Authorization: Bearer pse_..." \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
LAN のみでデプロイする場合も、これが唯一の方法です。パスキーの登録にはセキュアコンテキストが必要なため、通常の HTTP では登録できません。
ユーザーが確認・削除できる情報
/account には、ユーザーに関するすべての情報(メールアドレス、パスキー、有効なトークン、時間ごとの使用数)が表示されます。POST /account/delete を実行すると、承認手続きなしで直ちに完全削除されます。収集する情報とその保存期間は /privacy に記載されています。使用数は 90 日後に削除されます。
Docker Compose で実行する(HTTP + Postgres)
cp .env.example .env # set POSTGRES_PASSWORD
docker compose up --build
:8000 でストリーミング可能な HTTP を提供し、共有キャッシュとアーカイブには Postgres 18 を使用します。1 回限りの migrate サービスが、アプリの起動前に Alembic スキーマを適用します。
HTTP モードは、デフォルトで ステートレスなプレーン JSON 応答 を使用します。このサーバーが提供するのは、凍結ポリシーによって変更されないデータに対する読み取り専用ツールです。また、MCP セッションで利用できるようになる機能(通知、リソースのサブスクリプション、サンプリング、エリシテーション、進行状況)はどれも使用しないため、すべてのリクエストは自己完結しています。そのため、スティッキールーティング、セッションごとのメモリ、イベントストアを使わず、プレーンなラウンドロビン方式の背後にあるどのレプリカでも、すべてのリクエストを処理できます。SSE を使用しない場合、アイドル状態のクライアントが接続を保持することもありません。MCP セッションが必要な場合は --stateful を、イベントストリーム形式が必要な場合は --sse を使用してください。これらは独立したフラグです。
Bearer 認証とクォータ(オプトイン)
PSE_AUTH_REQUIRED=1(DATABASE_URL が必要)を設定すると、すべての HTTP リクエストに Authorization: Bearer <token> を含める必要があります。ユーザーは、ホスト型サーバーへの接続で説明するいずれかの方法で登録します。OAuth 2.1 とパスキーを使ったセルフサービス、または運用者が発行するトークンです。PKCE は必須(S256 のみ)で、システム内のどこにもパスワードは存在しません。
トークンは不透明で、保存されるのは SHA-256 ハッシュのみです。失効(pse-edge-admin revoke-token … / disable-user …)は、検証キャッシュの TTL(デフォルトで 60 秒)以内に反映されます。これが失効遅延の許容時間です。ユーザーごとのクォータ(デフォルトは 60/分、2,000/日。ユーザーごとに変更可能)はプロセス内でカウントされ、HTTP 429 と Retry-After を返します。レプリカが N 個ある場合、実効上限は公称値の最大 N 倍になりますが、悪用防止には問題ありません。stdio モードでは認証を行いません。ご自身のマシン上で実行されます。
運用者は pse-edge-admin delete-user と purge-usage を利用できます(後者は毎日 cron で実行してください)。また、delete-user はユーザー自身の削除ボタンと同じ消去コードパスを使うため、両者の動作にずれが生じることはありません。
Postgres は任意です。 DATABASE_URL がない場合、サーバーはインメモリキャッシュを使い、アーカイブを保持しません。これはローカルで stdio を使うためのゼロ設定の方法です。設定すると、レプリカ間で単一のキャッシュを共有します(凍結の仕組みにより、プロセス数にかかわらず、各境界で上流へのフェッチは 1 回だけです)。また、読み取りのたびにEOD アーカイブ(日次バーと開示情報)に蓄積され、PSE Edge に追加コストをかけずに時間とともに充実します。クローリングは行われません。アーカイブは、すでに実行したフェッチのみで蓄積されます。
# applying the schema by hand, outside compose
DATABASE_URL=postgresql+asyncpg://user:pass@host/db uv run alembic upgrade head
設定
すべての設定は環境変数から読み込まれ、単一の凍結された Settings オブジェクトにまとめられます。システムの構成を変える変数は 2 つあります。DATABASE_URL はストレージ層を選択し、PSE_AUTH_REQUIRED は認証スタック全体(および send_email ツール)を有効にします。
| 変数 | デフォルト | 制御する内容 |
|---|---|---|
| 上流 — PSE Edge を保護 | ||
PSE_EDGE_BASE_URL | https://edge.pse.com.ph | 上流ポータルのルート |
PSE_THROTTLE_RPS / PSE_THROTTLE_BURST | 1.0 / 2 | Edge へのトークンバケット方式のレート制限 |
PSE_TIMEOUT_SEC / PSE_RETRY_ATTEMPTS | 20 / 3 | リクエストごとのタイムアウトと再試行 |
| ストレージ — 唯一の切り替え | ||
DATABASE_URL | 未設定 | 未設定 → インメモリキャッシュ + NullArchive。設定 → 共有 Postgres キャッシュ + アーカイブ + 認証テーブル(スキーマ管理は Alembic のみ) |
PSE_DB_POOL_SIZE / PSE_DB_MAX_OVERFLOW | 5 / 10 | 接続プール |
認証 — オプトイン、DATABASE_URL が必要 | ||
PSE_AUTH_REQUIRED | 0 | Bearer 認証 + クォータ + OAuth/パスキー。stdio では認証を行いません |
PSE_TOKEN_CACHE_TTL | 60 | 失効遅延の許容時間。他の用途には使いません |
PSE_QUOTA_PER_MIN / PSE_QUOTA_PER_DAY | 60 / 2000 | ユーザーごとのクォータ。プロセス内(ワーカーごと)にカウント |
PSE_PUBLIC_URL | http://localhost:8000 | 実際の外部 https URL。WebAuthn の rp_id、メールリンク、OAuth の発行者に使用されます。値が誤っているとパスキーが動作しません |
PSE_ACCESS_TTL_MIN / PSE_REFRESH_TTL_HOURS | 15 / 24 | トークンの有効期間。リフレッシュトークンは 1 回限り使用でき、再利用するとファミリー全体が失効します。PSE_REFRESH_UNUSED_TTL_HOURS(24)は、一度もローテーションされないファミリーの有効期間を制限します |
PSE_ADMIN_EMAILS | 空 | /account のマシンクライアントパネルで使う運用者の許可リスト。ユーザー入力から取得することはありません |
| メールと運用 | ||
ZEPTOMAIL_API_KEY | 未設定 | 未設定 → ConsoleEmailSender。設定 → ZeptoMail |
PSE_EMAIL_FROM | no-reply@localhost | 送信者アドレス(ZeptoMail は完全一致するドメインを検証します) |
PSE_OPERATOR_EMAIL | 未設定 | Canary の失敗アラート。失敗時のみ通知し、「すべて正常」という通知は行いません |
PSE_USAGE_RETENTION_DAYS | 90 | 使用ログの保持期間(ユーザーごと・時間ごとに集計。リクエストごとの記録は保持しません) |
| サーバー | ||
PSE_STATEFUL / PSE_SSE | 0 / 0 | MCP セッションとレスポンスのモード |
PSE_LOG_JSON / PSE_LOG_LEVEL | 0 / INFO | どちらのフォーマッターもタイムスタンプを付加し、機密情報を伏せます。INFO ログに記録されるのは拒否のみです |
コンテナイメージ
mainへのマージごとにイメージを公開します。
docker pull ghcr.io/phdwight/pse-edge-mcp:latest # or :<version>, :sha-<sha>
# multi-arch: linux/amd64 and linux/arm64
docker run --rm -p 8000:8000 ghcr.io/phdwight/pse-edge-mcp:latest # streamable HTTP
docker run --rm -i --entrypoint pse-edge-mcp ghcr.io/phdwight/pse-edge-mcp:latest # stdio
公開前に、ネイティブランナー上で両アーキテクチャのゲートを実行します。基準はサイズではなく必要性です。イメージには、解決されたランタイム依存関係のクロージャだけを過不足なく含め、それ以外は何も含めてはいけません。つまり、ビルドツールチェーン、パッケージマネージャー、開発依存関係、バイトコードキャッシュ、ソースツリーは含めません。さらに、シークレットスキャンと、サーバーが起動してツールを登録することを確認するスモークテストも実施します。不要な依存関係があればビルドは失敗しますが、実際に必要な依存関係であればサイズが大きくても問題ありません。イメージサイズは情報として報告するだけで、ゲートの判定には使用しません。
本番環境
NAS または単一の Docker ホスト向けに、2 段階構成のファイル compose.nas.yaml を用意しています。ビルドするのではなく公開済みのイメージを取得するため、本番環境では CI のゲートを通過した成果物を実行します。ステージ 1 は LAN 専用で、Cloudflare は不要です。
docker compose -f compose.nas.yaml up -d # http://<nas-ip>:8200
docker compose -f compose.nas.yaml --profile tunnel up -d # + public hostname
tunnel プロファイルを起動すると、cloudflared が開始します。接続は外向きのため、ポートフォワーディングも ACME も不要で、CGNAT の影響も受けません。Cloudflare がエッジで TLS を終端します。CLOUDFLARE_TUNNEL_TOKEN、PSE_PUBLIC_URL、PSE_LAN_BIND=127.0.0.1 を .env に設定してください。最後の設定はステージ 1 の LAN ポートをループバックに移します。Compose は ports を加算的にマージするため、ポートを非公開にする方法はこれだけです。
どちらのステージも、認証がデフォルトで有効になっており、毎日のバックアップと毎日の保持期間に基づく削除が行われます。また、データベースのポートは公開されません。ヘルスプローブは /health(稼働状態)と /health/ready(準備状態)です。このアプリは他のサーバーからインポートできます。uvicorn pse_edge_mcp.asgi:app --workers 4
詳しいガイドは docs/deploy.md を参照してください。特に重要な設定は 2 つあります。:latest を追跡するのではなく PSE_IMAGE_TAG を固定すること、そして PSE_PUBLIC_URL に実際の外部 https URL を設定することです。WebAuthn では、すべてのパスキーが登録時のオリジンに紐付けられるためです。
開発
uv sync --all-extras
uv run pytest
uv run ruff check .
テストはすべて記録済みのフィクスチャに対して実行され、CI が PSE Edge にアクセスすることはありません。
このコードベースを初めて使う方へ。 docs/walkthrough.md には、開発者およびアーキテクト向けの解説があります。リクエストのライフサイクル、凍結ポリシー、レイヤー構成、ツールやデータドメイン全体の追加方法、症状から原因をたどるデバッグ表を掲載しています。PDF 版もあります。アーキテクチャセクションの 1 ページの図解版(クラス、プロトコル、データパス、設定マトリックス)は、docs/reference-card.html をブラウザーで開いてください。完全に自己完結しており、オフラインで利用できます。設計上の判断はすべて docs/plan.md に記録され、検証済みのエンドポイント一覧は docs/endpoints.md にあります。
貢献
Issue やプルリクエストを歓迎します。基本ルールは次のとおりです。
- 作業は
developに取り込み、プルリクエストでmainに反映します。mainは保護されており、3 つすべての CI チェック(test、image (amd64)、image (arm64))の通過が必要です。 - テストが PSE Edge にアクセスすることはありません。新しいエンドポイントには、
tests/fixtures/に新しい記録済みフィクスチャが必要です。 - 新しいツールは上記のレイヤー構成に従ってください。新しいデータドメインは、新しいリポジトリと薄いツールで構成し、
server.pyに取得・解析ロジックを実装しないでください。 pyproject.tomlのversionを更新すると、次のマージで GitHub Release と、それに対応する変更不可能なイメージタグが作成されます。同じプルリクエストでCHANGELOG.mdも更新してください。
ライセンス
MIT
MCP Registry の ID:mcp-name: io.github.phdwight/pse-edge-mcp
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。