关于 pse-edge-mcp
pse-edge-mcp 为关注菲律宾股市的研究者和金融应用提供 Philippine Stock Exchange 与 Bangko Sentral ng Pilipinas 数据。它涵盖行情、披露、财务和利率,并按数据类型采用不同的缓存与价格冻结策略。
主要功能
- 01查询股票报价和历史价格
- 02搜索披露并读取附件资源
- 03获取公司资料、财务及股息信息
- 04查看指数、市场摘要和 BSP 利率
- 05按数据类型缓存并标注陈旧状态
适用场景
- 01研究上市公司的历史行情与财务
- 02检索公司披露及其附件
- 03查看市场指数和央行关键利率
MCP 相关
MCP 服务向客户端公开只读金融数据工具、附件资源和两个提示词。启用身份验证的部署还提供仅向已认证用户本人发送邮件的操作工具。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
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
一个 MCP 服务,可向 Claude 和任何其他 MCP 客户端提供来自菲律宾证券交易所 PSE Edge 门户的数据,包括报价、价格历史、披露信息、财务报告和市场数据,以及菲律宾中央银行 Bangko Sentral ng Pilipinas 的关键利率。
非官方项目。 PSE Edge 和 BSP 均未提供公共 API;本项目访问的是其网页使用的相同端点。本项目与 PSE 或 BSP 无关,也未获其认可。数据按原样提供,仅供个人或研究用途,不作任何保证。
目录
- 功能
- 快速开始
- 设计:收盘价每日获取一次,其他数据仅获取一次
- 工具、资源、提示词
- 架构
- 黄金路径:跟踪一个请求
- 连接到托管服务
- 使用 Docker Compose 运行
- 配置
- 容器镜像
- 生产环境
- 开发
- 贡献
- 许可证
功能
- 15 个只读工具 + 1 个操作工具,涵盖报价、价格历史、披露信息(元数据、全文和详情)、公司简介、财务数据、股息、指数、市场概览以及BSP(中央银行)关键利率;另提供一个附件资源、两个支持代码补全的提示词,以及工具注释。
- 有意减少对 PSE Edge 的请求: 每个唯一查询每天最多访问一次,价格数据则严格遵循市场边界冻结策略。
- 人类用户可使用 OAuth 2.1 + 通行密钥(完全不使用密码),无头 Agent 可使用
client_credentials;两者均为可选,stdio 无需任何配置。 - Postgres 可选: 本地 stdio 模式下使用零配置内存存储;设置
DATABASE_URL后则使用共享缓存,并持续扩充收盘价档案。 - 及时发现变化: 每晚运行的金丝雀测试会用真实模型验证实时网页,仅在失败时发出提醒;页面样式变化会引发错误,绝不返回不完整数据。
- 多架构容器镜像(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
设计:收盘价,其余内容只获取一次
四层架构,依赖关系单向。每次读取都通过 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?) | 全市场或按公司查询披露元数据;每页 50 条,并提供准确总数 |
search_disclosure_fulltext(keyword, ...) | 搜索披露附件中的文本,并提供摘要片段 |
get_disclosure(edge_no, max_files?) | 获取一条披露的详情,以及附件和正文 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(中央银行)关键利率仪表板:政策利率、便利工具利率和拍卖利率、总体通胀率,以及美元兑比索参考汇率 |
get_bsp_policy_rate() | BSP 政策利率(Target RRP)及其周边的隔夜贷款/存款利率区间 |
get_server_version() | 此 MCP 服务当前部署的版本(与 /health 一致) |
send_email(subject, body) | 给自己发送邮件便笺(仅限启用身份验证的部署) |
除工具外,该服务还提供上文所述的附件资源、两个提示词(market_recap、company_briefing(symbol)——symbol 参数会根据 PSE Edge 自带的查询功能自动补全),以及 MCP 工具注解,以便宿主自动批准只读工具。它在 server.json 中为 MCP Registry 提供了说明。
send_email 是唯一会执行操作而非只读取数据的工具。它没有收件人参数:邮件始终发送给已验证该会话身份的账户,因此不能用作中继,也没有可供提示词注入改写的收件人——鉴于此服务会返回运营者无法控制的披露文本,这一点很重要。只有启用身份验证的部署才会提供此工具(否则没有经过验证的地址);邮件正文会经过转义,而非渲染为 HTML;每位用户每天最多可发送 20 封邮件。
披露工具只返回元数据和链接——除明确读取资源外,此服务绝不会下载或解析附件,因此如果需要文件,你的 MCP 客户端可以自行获取返回的 URL。请注意,Edge 自带的全文索引并不完整(根据最近一次检查,大致覆盖 2023–2025 年),因此 search_disclosure_fulltext 不能替代 search_disclosures;其结果中会说明这一限制。
财务数据会按照 PSE Edge 显示的原样返回,绝不会重新缩放——Edge 在年度和季度部分使用的单位标签并不一致,因此每个期间都会报告 currency_units,供你核对。此处的指数变动带有正负号,尽管 Edge 显示时不标明正负(仅通过颜色和箭头表示方向)。
架构
各层
| 层 | 职责 | 不负责 |
|---|---|---|
server.py — MCP 边界 | 参数验证、委派、回复整形。错误映射只在 reply() 中进行一次;操作工具通过 act() 处理(无 freshness meta)。 | 领域逻辑、缓存键、解析、端点选择 |
repositories.py — 领域层 | 每个数据领域对应一个仓库:缓存键、冻结数据读取、解析、Pydantic 模型。端点路由在此处理。 | 依赖具体客户端——仅依赖下文的协议 |
service.py / sources.py — 策略与接缝 | FreezeService 强制执行每次读取策略,并将每个结果包装为 Served[T](值 + as_of、valid_until、from_cache、stale)。sources.py 声明五个范围精简的领域级源协议;FrozenCache 是缓存接缝。 | — |
client.py / parsers.py — 外部边界 | 纯 HTTP,与 MCP 无关:令牌桶限流、single-flight、重试;两种请求格式(用于图表 .ax 端点的 JSON 请求体 POST,以及用于 search.ax、返回 HTML 片段的表单编码 POST)。解析器将 HTML/JSON 转为字典;任何结构变化都会引发 EndpointChangedError——明确报错,绝不返回不完整结果。 | — |
核心类一览
五个仓库涵盖全部工具接口。每个仓库都使用一个范围精简的源协议——具体客户端实现了全部五个协议,但仓库并不知道这一点,因此每个仓库都可以用几行代码编写的伪对象进行测试,无需模拟 HTTP。
| 仓库 | 方法 → 模型 | 使用的协议 | 策略 / 说明 |
|---|---|---|---|
CompanyRepository | search、resolve、try_resolve → CompanyHit | CompanySource | daily-refresh · 将 symbol 解析为 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 | — | 收件人来自持有者令牌,绝不作为参数传入 |
协议与可替换实现
一个开关决定所用列:未设置 DATABASE_URL → 内存 / Null;已设置 → Postgres。Postgres 模块采用延迟导入,因此轻量安装无需承担这些模块的开销。
| 协议 | 未设置 DATABASE_URL | 已设置 DATABASE_URL |
|---|---|---|
Storage | InMemoryStorage | PostgresStorage |
Archive | NullArchive | PostgresArchive |
UsageSink | NullUsageRecorder | PostgresUsageSink |
AuthStore | — | PostgresAuthStore |
EmailSender | ConsoleEmailSender | ZeptoMailSender(设置了 ZEPTOMAIL_API_KEY 时) |
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 + Web 会话)、TokenService(不透明的 pse_ 令牌,静态存储时使用 SHA-256)。
错误类别——统一根类,在 reply() 中映射一次
| 错误 | 含义 |
|---|---|
SymbolNotFoundError | SYMBOL_NOT_FOUND |
InvalidArgumentError | INVALID_ARGUMENT — 输入无效,在边界处捕获 |
EndpointChangedError | Edge 重新设计了页面——明确报错,绝不返回不完整结果 |
EdgeUnavailableError | 上游不可访问,且没有缓存数据 |
MarketOpenNoCacheError | 为兼容客户端而保留;不再引发 |
ActionUnavailableError | 操作工具需要启用身份验证 |
ActionRateLimitedError | 每位用户每天 20 封邮件 |
Watchdog
每晚的 canary(pse-edge-canary,以及一个 compose 服务)会绕过缓存获取实时页面,并验证仓库构建时使用的同一套 Pydantic 模型——表格样式改变后仍返回 200,正是它要捕获的故障。市场开放期间它仍会拒绝运行(★ 不变量的优先级更高),仅在失败时向 PSE_OPERATOR_EMAIL 发送邮件,并以非零状态退出,以便 cron 注意到。
黄金路径:追踪一个请求
收盘后,缓存未命中时调用 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,绝不重新获取。市场开放且从未缓存 → 获取一次,并在整个交易时段标记stale: true和note。市场关闭且未命中 → 获取。获取失败但存在过期条目 → 返回该条目并标记为stale。PseEdgeClient.fetch_stock_data_page(company_id)— 令牌桶(1 req/s)、单航班去重、重试。传输日期格式为MM-dd-yyyy;根据端点选择 JSON 或表单格式。parsers.py→StockQuote— HTML → 字典 → 经过验证的 Pydantic 模型。Edge 标记语言的任何变化都会引发EndpointChangedError。cache.py/archive.py— 缓存条目会冻结至次日 15:00 收盘;每日行情数据会择机归档(数据库故障绝不会导致读取失败)。
连接到托管服务
启用身份验证的部署是受标准 OAuth 2.1 保护的资源,因此现代 MCP 客户端只需要 URL——它会自行发现其他所有信息并完成整个流程。
首次连接时会发生什么
除了加粗标出的两个浏览器步骤,其他操作均无需手动完成。
- 客户端在没有令牌的情况下向
/mcp发送POST请求,并收到 401,响应中带有WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"。该标头包含了完整的引导信息:它会告诉客户端接下来去哪里查找。 - 客户端获取该文档,了解保护此资源的授权服务器,然后读取
/.well-known/oauth-authorization-server获取端点信息。 - 客户端在
/oauth/register注册自身(RFC 7591)——无需客户端密钥、无需操作员介入,也无需预先共享凭据。注册后会获得一个client_id。 - 客户端在浏览器中打开
/oauth/authorize,并附带 PKCE 挑战(必须使用 S256)。 - 用户注册或登录。 新用户会进入
/signup,同意(有意保持精简的)数据政策,提供电子邮件地址并收到一个链接;该链接会显示一个确认页面,其按钮会在/enroll注册通行密钥——设置确认步骤是为了避免邮件扫描器预取链接并消耗它。回访用户会进入/login,使用已有的通行密钥。整个系统中不存在密码。 - 用户在显示客户端名称的授权页面上批准该客户端。
- 浏览器将一次性代码返回给客户端;客户端使用 PKCE 验证码在
/oauth/token交换该代码,并获取一个访问令牌(15 min)和一个一次性刷新令牌(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)——被盗的刷新令牌只能使用一次,随后盗用行为就会被检测到,会话也会终止。
无头 Agent(client_credentials)
适用于 LangGraph 应用、Anthropic Messages API MCP 连接器,或任何无法打开浏览器的 Agent。无需重定向、无需通行密钥、无需同意页面——只需客户端 ID 和密钥。
1. 配置。 两种途径,结果相同:
- 通过网页(无需使用 shell——在 NAS 上更实用):将
PSE_ADMIN_EMAILS设置为你的账户邮箱,登录后,/account上会出现 Machine clients 面板,其中提供创建和撤销控件。访问权限仅限允许列表中的账户——普通注册用户看不到该面板。 - 通过 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 都会这样发送。不会签发刷新令牌——客户端已经持有长期有效的密钥,令牌到期后只需重新请求即可。
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 写入,不会根据注册者对自身的描述来决定。
请为每个 Agent 单独创建一个机器客户端:配额按客户端计算,因此失控的任务只会限制自身;撤销一个客户端也不会影响其他客户端。
要基于此构建应用? examples/langgraph_client.py 是适用于多租户场景的可运行客户端——你的应用使用一个机器客户端,以自身身份进行认证,用户无需接触此服务。它包含一个 httpx.Auth,用于获取并刷新 1 小时有效的令牌(已验证:并发调用只会获取一次令牌;令牌过期时会在收到 401 后恢复),以及值得粘贴到系统提示词中的 Agent 指令。请注意,它需要 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"}'
这也是仅限局域网的部署方式中唯一可用的途径:通行密钥需要安全上下文,因此无法通过普通 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 作为共享缓存和归档。一项仅运行一次的 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 s,这就是精确的撤销延迟预算。按用户计算的配额(默认每分钟 60 次、每天 2,000 次,可按用户覆盖)在进程内计数,超出时返回 HTTP 429 和 Retry-After;有 N 个副本时,实际上限最高可达名义值的 N 倍,这足以防止滥用。stdio 模式从不进行身份验证——它在你自己的机器上运行。
运营方可使用 pse-edge-admin delete-user 和 purge-usage(后者应设为每日 cron 任务);delete-user 使用与用户自行点击删除按钮相同的数据擦除代码路径,因此两者不会出现偏差。
Postgres 是可选的。 未设置 DATABASE_URL 时,服务使用内存缓存且不保留归档——这是本地 stdio 使用的零配置路径。设置后,多个副本会共享一个缓存(无论运行多少个进程,冻结机制仍确保每个边界仅请求一次上游),并且每次读取都会累积到 EOD 归档(每日行情和披露)中,使归档随时间不断丰富,而不会给 PSE Edge 带来额外成本。系统不会主动抓取数据——归档仅通过你已经发起的请求逐步填充。
# applying the schema by hand, outside compose
DATABASE_URL=postgresql+asyncpg://user:pass@host/db uv run alembic upgrade head
配置
所有配置都来自环境变量,并汇总到一个冻结的 Settings 对象中。两个变量会改变系统的形态: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 issuer;值错误会导致通行密钥失效 |
PSE_ACCESS_TTL_MIN / PSE_REFRESH_TTL_HOURS | 15 / 24 | 令牌有效期;刷新令牌只能使用一次,重复使用会撤销整个令牌族。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 | 未设置 | 金丝雀失败告警——仅报告失败,绝不发送“一切正常”通知 |
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
发布前会在原生运行器上对两种架构进行门禁检查。规则是必要性,而非大小:镜像必须恰好包含解析后的运行时依赖闭包,不得包含其他内容——不能有构建工具链、包管理器、开发依赖、字节码缓存或源代码树——此外还要通过机密扫描和冒烟测试,确认服务能够启动并注册其工具。多余的依赖会导致构建失败;真正必需的依赖即使很大也不会。镜像大小仅供参考,绝不会作为门禁条件。
生产环境
一个文件 compose.nas.yaml,适用于 NAS 或任何单台 Docker 主机,分两个阶段运行。它拉取已发布的镜像,而不是自行构建,因此生产环境运行的正是通过 CI 门禁的制品。第 1 阶段仅限局域网使用,不需要 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。在 .env 中与其一起设置 CLOUDFLARE_TUNNEL_TOKEN、PSE_PUBLIC_URL 和 PSE_LAN_BIND=127.0.0.1——最后一项会将第 1 阶段的局域网端口移到环回地址,这是取消发布端口的唯一方法,因为 Compose 会以累加方式合并 ports。
两个阶段默认启用身份验证、每日备份和每日保留策略清理,并且不会发布数据库端口。健康探针分别使用 /health(存活状态)和 /health/ready(就绪状态)。此应用也可供其他服务导入:uvicorn pse_edge_mcp.asgi:app --workers 4。
完整指南请参阅 docs/deploy.md,其中详细介绍了最值得正确设置的两项配置:固定 PSE_IMAGE_TAG,而不是跟踪 :latest;并将 PSE_PUBLIC_URL 设为真实的外部 https URL,因为 WebAuthn 会将每个通行密钥绑定到其注册时使用的源。
开发
uv sync --all-extras
uv run pytest
uv run ruff check .
测试完全基于已记录的固定数据运行——CI 从不访问 PSE Edge。
初次接触此代码库? docs/walkthrough.md 是面向开发者和架构师的导览,介绍请求生命周期、冻结策略、分层结构、如何添加工具或完整的数据域,以及症状到原因的调试表。另有 PDF 版本。如需查看 架构 部分的一页式可视化版本——包含类、协议、数据路径和配置矩阵——请在任意浏览器中打开 docs/reference-card.html;该文件完全自包含,可离线使用。每项设计决策都记录在 docs/plan.md 中,经过验证的端点映射则位于 docs/endpoints.md。
贡献
欢迎提交 Issue 和拉取请求。基本规则如下:
- 工作在
develop上进行,并通过拉取请求合并到main;main受保护,必须通过全部三项 CI 检查(test、image (amd64)、image (arm64))。 - 测试绝不访问 PSE Edge——新增端点需要在
tests/fixtures/中添加新的已记录固定数据。 - 新工具须遵循上述分层结构:新增数据域时,应新建一个仓库并添加轻量工具,绝不能在
server.py中编写获取或解析逻辑。 - 在
pyproject.toml中提升version会使下一次合并生成一个 GitHub Release,并附带对应的不可变镜像标签;请在同一个 PR 中更新CHANGELOG.md。
许可证
MIT
MCP 注册表标识:mcp-name: io.github.phdwight/pse-edge-mcp
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。