Vire门户
Vire投资平台的Web应用程序和MCP服务器。托管登录页面,在以下位置为MCP工具端点提供服务 /mcp,并将所有工具调用代理到vire服务器。
该门户是一个Go服务器,它使用Alpine.js渲染HTML模板以实现交互性,并为Claude和其他MCP客户端提供MCP(模型上下文协议)端点。它将工具调用代理到vire服务器,为用户上下文注入X-vire-\*标头。从Docker容器和vire服务器一起提供服务。
存储库布局: 此仓库包含门户服务器和MCP网桥代码。Docker镜像(ghcr.io/bobmcallan/vire-portal:latest和ghcr.io/bobmcallan/vire-mcp:latest)已发布给GHCR。入口沿着ghcr.io/bobmcallan/vire-server:latest在Docker Compose堆栈中。vire-mcp是Claude Desktop推出的stdio到HTTP桥,它通过OAuth 2.1连接到vire门户的MCP端点,并通过stdio重新公开工具。门户基础设施(云运行部署)由以下人员管理 病毒 地形(infra/modules/portal/).
技术栈
- 转到1.25+ 符合标准
net/http(无框架) - 走
html/template用于服务器端渲染 - Alpine.js (CDN)用于客户端交互
- Chart.js v4 (CDN)用于投资组合增长图
- 无状态 --由病毒服务器通过REST API管理的所有用户数据
- TOML 具有优先级的配置:默认值\环境变量>TOML文件>默认值。
| 设置 | TOML键 | 环境变量 | CLI标志 | 默认值 |
|---|---|---|---|---|
| 服务器端口 | server.port | VIRE_SERVER_PORT | -port, -p | 8080 |
| 服务器主机 | server.host | VIRE_SERVER_HOST | -host | localhost |
| API URL | api.url | VIRE_API_URL | -- | http://localhost:8080 |
| JWT机密 | auth.jwt_secret | VIRE_AUTH_JWT_SECRET | -- | "" |
| OAuth回调URL | auth.callback_url | VIRE_AUTH_CALLBACK_URL | -- | http://localhost:8080/auth/callback |
| 门户URL | auth.portal_url | VIRE_PORTAL_URL | -- | "" |
| 管理员用户 | admin_users | VIRE_ADMIN_USERS | -- | "" |
| 服务密钥 | service.key | VIRE_SERVICE_KEY | -- | "" |
| 门户ID | service.portal_id | VIRE_PORTAL_ID | -- | 主机名 |
| 环境 | environment | VIRE_ENV | -- | prod |
| 日志级别 | logging.level | VIRE_LOG_LEVEL | -- | info |
| 日志格式 | logging.format | VIRE_LOG_FORMAT | -- | text |
| 日志输出 | logging.outputs | -- | -- | ["console", "file"] |
| 日志文件路径 | logging.file_path | -- | -- | logs/vire-portal.log |
| 日志最大大小(MB) | logging.max_size_mb | -- | -- | 10 |
| 记录最大备份数 | logging.max_backups | -- | -- | 5 |
配置文件是从以下位置自动发现的 vire-portal.toml 或 docker/vire-portal.toml。用以下方式明确指定 -c path/to/config.toml.
这 [api] 部分配置MCP代理。 api.url 指向vire服务器实例。用户上下文作为X-Vire-\*标头注入到每个代理请求中。所有用户数据均由vire服务器管理。
MCP端点
门户在以下位置托管MCP(模型上下文协议)服务器 POST /mcp 使用 mcp走 使用流式HTTP传输。Claude和其他MCP客户端连接到此端点以访问投资工具。
建筑
Claude Code / CLI Claude Desktop
| |
| POST /mcp (Streamable HTTP) | stdio
| |
| vire-mcp (bridge)
| |
| | POST /mcp (HTTP + OAuth)
v v
vire-portal (:8881)
| internal/mcp/ package
| - Dynamic tool catalog from GET /api/mcp/tools
| - Generic proxy handler (path/query/body params)
| - X-Vire-* header injection from JWT sub claim
| - OAuth 2.1 (DCR, PKCE, token exchange)
v
vire-server (:8080)启动时,门户从vire服务器获取工具目录 GET /api/mcp/tools 端点重试(3次尝试,2秒回退)。每个目录条目都定义了工具名称、描述、HTTP方法、URL路径模板和参数。门户验证每个条目(非空名称/方法/路径、方法白名单、, /api/ 路径前缀,无路径遍历)并跳过重复项。有效的工具会动态注册为MCP工具,并路由到适当的REST端点。如果在所有重试后都无法访问vire服务器,则门户将以0个工具(非致命)启动。
所有工具调用都代理到vire服务器。门户不解析或格式化响应——它从vire服务器返回原始JSON,让MCP客户端(Claude)格式化输出。
连接克劳德
该门户通过Streamable HTTP为MCP提供服务,网址为 POST /mcp 使用OAuth 2.1身份验证。所有工具逻辑、目录管理和版本处理都在vire门户中。客户端可以直接通过HTTP(Claude Code、Claude CLI)或通过 vire-mcp stdio桥接器(克劳德桌面)。
Claude Desktop → stdio → vire-mcp → HTTP + OAuth → vire-portal (:8881/mcp) → vire-server
Claude CLI/Web → HTTP + OAuth → vire-portal (:8881/mcp) → vire-server运输兼容性
| 客户端 | 传输 | 如何连接 |
|---|---|---|
| Claude Code(CLI) | 可流式HTTP | 将HTTP URL直接发送到门户 |
克劳德桌面(claude_desktop_config.json) 。 vire-mcp 二进制或Docker | ||
| 克劳德桌面(连接器UI) | HTTPS | 公共HTTPS URL(生产) |
克劳德代码(CLI)
Claude Code原生支持流式HTTP和MCP OAuth。在第一次连接时,Claude Code会自动运行OAuth流(打开浏览器、用户登录、接收令牌)。
添加 ~/.claude.json (全球)或项目 .mcp.json (每个项目):
{
"mcpServers": {
"vire": {
"type": "url",
"url": "http://localhost:8881/mcp"
}
}
}对于已部署的HTTPS门户:
{
"mcpServers": {
"vire": {
"type": "url",
"url": "https://portal.vire.app/mcp"
}
}
}Claude Desktop(通过vire mcp进行本地开发)
Claude Desktop需要本地MCP服务器的stdio传输。 vire-mcp 是一个stdio到HTTP的网桥,它作为MCP客户端连接到vire门户,发现工具,并通过stdio重新公开它们。
两种连接方式:
| 模式 | 环境变量 | Auth | 用例 |
|---|---|---|---|
| OAuth | VIRE_PORTAL_URL | 浏览器OAuth流,缓存令牌 | 具有交互式登录的本地开发人员 |
| 直接的 | VIRE_MCP_URL | URL中的加密UID(无浏览器) | Docker、CI/CD、无头环境 |
配置:
vire-mcp 自动从以下位置发现其TOML配置 vire-mcp.toml 或 config/vire-mcp.toml (首先检查二进制相对路径)。环境变量覆盖配置值:
| 环境变量 | 默认值 | 描述 |
|---|---|---|
VIRE_PORTAL_URL | http://localhost:8080 | vire门户URL(OAuth模式) |
VIRE_MCP_URL | -- | 带有加密UID的完整MCP端点URL(直接模式,绕过OAuth) |
VIRE_LOG_LEVEL | info | 日志级别(调试、信息、警告、错误) |
日志被写入 bin/logs/vire-mcp.log (相对于二进制)。启动日志显示解析的URL和模式:
level=INF message="loaded configuration" direct_mode=true mcp_url="http://localhost:8881/mcp/abc123..."
level=INF message="loaded configuration" direct_mode=false portal_url="http://localhost:8881"直接模式(推荐用于Docker/WSL)
直接模式使用嵌入用户身份的加密MCP端点URL,完全绕过OAuth。从vire门户仪表板(配置文件>MCP配置)获取您的唯一端点URL。
{
"mcpServers": {
"vire": {
"command": "/path/to/bin/vire-mcp",
"env": {
"VIRE_MCP_URL": "http://localhost:8881/mcp/YOUR_ENCRYPTED_UID",
"VIRE_LOG_LEVEL": "error"
}
}
}
}OAuth模式(交互式登录)
OAuth模式在首次启动时运行浏览器流;令牌缓存在 ~/.vire/credentials.json.
{
"mcpServers": {
"vire": {
"command": "/path/to/bin/vire-mcp",
"env": {
"VIRE_PORTAL_URL": "http://localhost:8881"
}
}
}
}Windows上的WSL(直接模式)
在WSL中使用vire-mcp在Windows上运行时,使用shell包装器传递环境变量( env 对象未通过 wsl -e):
{
"mcpServers": {
"vire": {
"command": "wsl",
"args": [
"-e",
"/bin/bash",
"-c",
"VIRE_MCP_URL=http://localhost:8881/mcp/YOUR_ENCRYPTED_UID VIRE_LOG_LEVEL=error /home/bobmc/development/vire-portal/bin/vire-mcp"
]
}
}
}注: 这envClaude Desktop配置中的对象不传递变量wsl -e.使用/bin/bash -c "VAR=val command"相反。
Docker(直接模式-短暂)
直接模式非常适合Docker:没有OAuth,没有凭据挂载,完全是短暂的:
{
"mcpServers": {
"vire": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"--network", "host",
"-e", "VIRE_MCP_URL=http://localhost:8881/mcp/YOUR_ENCRYPTED_UID",
"-e", "VIRE_LOG_LEVEL=error",
"ghcr.io/bobmcallan/vire-mcp:latest"
]
}
}
}Docker(OAuth模式-持久凭据)
OAuth模式要求卷装载以在容器运行中持久化令牌:
{
"mcpServers": {
"vire": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"--network", "host",
"-e", "VIRE_PORTAL_URL=http://localhost:8881",
"-v", "~/.vire:/root/.vire",
"ghcr.io/bobmcallan/vire-mcp:latest"
]
}
}
}注: 对于远程门户,请替换localhost:8881使用您的门户URL(例如。,https://portal.vire.app).
克劳德桌面(通过连接器制作)
对于已部署的HTTPS门户(例如。 https://portal.vire.app),通过添加服务器 设置>连接器 在克劳德桌面。连接器支持HTTPS URL并原生处理OAuth——否 vire-mcp 桥需要。
MCP OAuth 2.1流程
该门户使用PKCE S256和动态客户端注册实现了OAuth 2.1(RFC 9728)。此流由以下因素自动触发 mcp-remote 或克劳德桌面连接器:
1. Client discovers endpoints: GET /.well-known/oauth-authorization-server
2. Client registers: POST /register (Dynamic Client Registration)
3. Client redirects user: GET /authorize?client_id=...&code_challenge=...&state=...
4. Portal creates a pending session, sets mcp_session_id cookie, redirects to /
5. User logs in (email/password or Google/GitHub OAuth)
6. On login success, portal detects mcp_session_id cookie
7. Portal creates auth code, redirects to client's redirect_uri
8. Client exchanges code + code_verifier: POST /token
9. Portal verifies PKCE S256, mints JWT access token (1h) + refresh token (7d)
10. Client sends Authorization: Bearer on all subsequent MCP requests身份验证链
Claude (Bearer token or cookie)
-> POST /mcp
-> vire-portal: extract user ID from JWT "sub" claim
-> proxy to vire-server with X-Vire-User-ID header
-> vire-server: look up user's Navexa key from DB
-> call Navexa API with user's key门户从以下任一来源提取用户身份:
Authorization: Bearer标题(Claude Desktop通过mcp远程/连接器)vire_sessioncookie(网络仪表板)
承载令牌优先。如果两者都不存在,则请求将在没有用户上下文的情况下继续进行。
工具
工具从vire服务器动态注册 GET /api/mcp/tools 启动时的目录。目录定义了每个工具的名称、描述、HTTP方法、URL路径模板和参数(路径、查询、正文)。门户从目录条目构建MCP工具定义和通用处理程序。请参阅 vire服务器README 查看完整的工具目录。
X-Vire-\*接头
代理在每次向vire服务器发出请求时都会注入这些标头:
| 标题 | 来源 | 描述 |
|---|---|---|
X-Vire-Portfolios | VIRE_DEFAULT_PORTFOLIO env var | 逗号分隔的投资组合名称 |
X-Vire-Display-Currency | VIRE_DISPLAY_CURRENCY env var | 显示值的货币 |
X-Vire-User-ID | 会话cookie(按请求) | JWT子声明中的用户名 |
静态标头是在每次请求时从环境变量中设置的。当 vire_session cookie存在——处理程序解码JWT子声明并注入用户ID。vire服务器从用户ID内部解析用户的navexa密钥。
认证流程
门户通过vire服务器对用户进行身份验证。支持三种登录方式:电子邮件/密码、Google OAuth和GitHub OAuth。vire服务器处理凭证验证和令牌交换;门户从不直接接触密码或OAuth机密。
电子邮件/密码登录
1. User submits email + password on the landing page
2. Portal forwards to vire-server: POST /api/auth/login { username, password }
3. vire-server validates credentials, returns a signed JWT
4. Portal sets the JWT as an httpOnly "vire_session" cookie
5. User is redirected to /dashboardOAuth登录(谷歌/GitHub)
1. User clicks "Sign in with Google" (or GitHub) on /
-> Browser navigates to portal: GET /api/auth/login/google
2. Portal makes a server-side request to vire-server: GET {API_URL}/api/auth/login/google?callback={callbackURL}
3. vire-server returns a 302 redirect to the OAuth provider's consent screen
4. Portal forwards the redirect Location to the browser (never exposing internal API URLs)
5. User authorises, provider redirects back to vire-server
6. vire-server exchanges code for tokens, creates/updates user, mints a JWT
7. vire-server redirects to portal: GET /auth/callback?token=
8. Portal sets the JWT as an httpOnly "vire_session" cookie
9. User is redirected to /dashboard服务器端代理阻止内部Docker地址(如 http://server:8080)避免暴露在浏览器中。
这 callback_url 配置设置告诉门户在OAuth完成后,病毒服务器应重定向到何处。这必须与每个OAuth提供者注册的URL匹配。
MCP OAuth 2.1流程(克劳德桌面版)
当Claude Desktop连接时,它会使用PKCE执行完整的OAuth 2.1流程:
1. Claude Desktop discovers endpoints: GET /.well-known/oauth-authorization-server
2. Claude Desktop registers: POST /register (Dynamic Client Registration)
3. Claude Desktop redirects user: GET /authorize?client_id=...&code_challenge=...&state=...
4. Portal creates a pending session, sets mcp_session_id cookie, redirects to /
5. User logs in (email/password or OAuth — same flows as above)
6. On login success, portal detects mcp_session_id cookie
7. Portal calls CompleteAuthorization: creates auth code, redirects to Claude Desktop's redirect_uri
8. Claude Desktop exchanges code + code_verifier: POST /token
9. Portal verifies PKCE S256, mints a JWT access token (1h) + refresh token (7d)
10. Claude Desktop sends Bearer token on all subsequent MCP requestsJWT索赔
门户OAuth服务器生成的令牌包含:
| 索赔 | 描述 |
|---|---|
sub | 用户ID(来自病毒服务器) |
scope | 授权范围(例如。 openid portfolio:read tools:invoke) |
client_id | OAuth客户端ID |
iss | 门户基础URL |
iat / exp | 签发/到期(1小时) |
令牌是使用HMAC-SHA256签名的 auth.jwt_secret 配置值。
OAuth提供程序配置
| 提供商 | 范围 |
|---|---|
谷歌 openid, email, profile | |
| GitHub | read:user, user:email |
API与病毒服务器的合同
门户只与病毒服务器的REST API进行通信。所有请求/响应体都是JSON。
错误响应格式
所有端点都以一致的形状返回错误:
{
"error": {
"code": "invalid_key",
"message": "EODHD API returned 401 -- check that the key is correct and has an active subscription"
}
}| HTTP状态 | 含义 | 门户操作 |
|---|---|---|
| 400 | 错误请求(缺少必填字段) | 显示验证错误 |
| 401 | JWT已过期或无效 | 尝试刷新令牌;如果刷新失败,请重定向到 / |
| 403 | 禁止(帐户已挂起) | 显示帐户状态消息 |
| 404 | 找不到资源 | 显示“找不到”状态 |
| 409 | 冲突(例如,已配置) | 幂等处理(显示现有资源) |
| 422 | 验证失败(例如,API密钥无效) | 显示响应中的字段级错误 |
| 429 | 速率受限 | 显示重试消息 |
| 500 | 服务器错误 | 显示带有重试选项的一般错误 |
身份验证路由(未经身份验证)
GET /api/auth/login/:provider
通过vire服务器代理OAuth重定向。门户向vire服务器发出服务器端HTTP请求,vire服务器向OAuth提供者返回302重定向。门户将重定向位置转发到浏览器。这可以防止内部Docker地址到达浏览器。
| 参数 | 类型 | 说明 |
|---|---|---|
:provider | 路径 | google 或 github |
答复: 302重定向到提供商OAuth URL(从vire服务器转发)。错误:302重定向到 /error?reason=auth_unavailable 或 /error?reason=auth_failed.
POST /api/auth/callback
为会话交换OAuth授权码。这 state 参数由网关在以下期间生成 GET /api/auth/login/:provider 并通过OAuth流程。
请求正文:
{
"provider": "google",
"code": "4/0AY0e-g...",
"state": "value-from-callback-query-params"
}答复(200):
{
"token": "eyJhbG...",
"user": {
"user_id": "uuid",
"email": "alice@example.com",
"display_name": "Alice",
"avatar_url": "https://...",
"auth_provider": "google",
"created_at": "2026-02-09T10:00:00Z",
"status": "active",
"keys_configured": false,
"plan": "free"
}
}还设置 refresh_token 如httpOnly、Secure、SameSite=Lax cookie。
POST /api/auth/refresh
使用httpOnly刷新令牌cookie刷新过期的JWT。
请求正文: 无(cookie自动发送)。
答复(200):
{
"token": "eyJhbG..."
}回复(401): 刷新令牌已过期或无效。用户必须重新进行身份验证。
POST /api/auth/logout
清除刷新令牌cookie并使会话无效。
答复(200):
{
"status": "ok"
}配置文件路线(需要JWT)
GET /api/profile
返回经过身份验证的用户的个人资料。
答复(200):
{
"user_id": "uuid",
"email": "alice@example.com",
"display_name": "Alice",
"avatar_url": "https://...",
"auth_provider": "google",
"created_at": "2026-02-09T10:00:00Z",
"status": "active",
"keys_configured": true,
"default_portfolio": "SMSF",
"portfolios": ["SMSF", "Personal"],
"exchange": "AU",
"plan": "pro",
"proxy_url": "https://vire-mcp-a1b2c3-xyz.run.app",
"proxy_status": "running",
"provisioned_at": "2026-02-09T10:01:00Z"
}PUT /api/profile
更新用户首选项。
请求正文(部分更新——仅包括要更改的字段):
{
"default_portfolio": "Personal",
"portfolios": ["SMSF", "Personal"],
"exchange": "AU"
}可编辑字段: default_portfolio, portfolios, exchange.其他配置文件字段(log_level, display_name, avatar_url)存在于Firestore数据模型中,但在第一阶段中不会公开用于门户编辑。标识字段(email, auth_provider, user_id)基础设施领域(proxy_url, provisioned_at)始终是只读的。
答复(200): 更新了配置文件对象(形状与GET/api/profile相同)。
DELETE /api/profile
删除用户帐户。触发MCP代理的取消配置、秘密清理和数据删除(30天宽限期)。
答复(200):
{
"status": "deleted",
"grace_period_ends": "2026-03-11T10:00:00Z"
}API关键路线(需要JWT)
PUT /api/profile/keys
设置或更新一个或多个API密钥(BYOK)。网关在存储之前根据提供程序的API验证每个密钥。密钥存储在密钥管理器中,而不是存储在Firestore中。
请求正文:
{
"eodhd_key": "abc123...",
"navexa_key": "def456...",
"gemini_key": "ghi789..."
}所有字段都是可选的——只包括要设置或更新的键。
答复(200):
{
"eodhd_key": {
"status": "valid",
"last4": "c123",
"validated_at": "2026-02-09T10:05:00Z"
},
"navexa_key": {
"status": "valid",
"last4": "f456",
"validated_at": "2026-02-09T10:05:00Z",
"portfolios_found": 2
},
"gemini_key": {
"status": "valid",
"last4": "i789",
"validated_at": "2026-02-09T10:05:00Z"
}
}响应(422)--验证失败:
{
"eodhd_key": {
"status": "invalid",
"error": "API returned 401 -- check that the key is correct and has an active subscription"
}
}关键验证方法:
| 提供者 | 验证 | 终结点 |
|---|---|---|
| EODHD | GET /api/exchanges-list/?api_token={key} | eostoricaldata.com |
| 航海 | GET /v1/portfolios 随着 X-API-Key 标题 | navexa.io |
| 双子座 | models.list 通过SDK | generativelanguage.googleapis.com |
DELETE /api/profile/keys/:id
删除特定的API密钥。
| 参数 | 类型 | 说明 |
|---|---|---|
:id | 路径 | eodhd_key, navexa_key,或 gemini_key |
答复(200):
{
"status": "removed",
"key_name": "navexa_key"
}供应路线(需要JWT)
POST /api/profile/provision
为用户提供专用的MCP代理(云运行服务)。至少需要 eodhd_key 待配置。
答复(200):
{
"status": "provisioned",
"proxy_url": "https://vire-mcp-a1b2c3-xyz123abc-ts.a.run.app",
"provisioned_at": "2026-02-09T10:01:00Z"
}答复(400): EODHD密钥未配置。
答复(409): 已配置(返回现有的proxy_url)。
GET /api/profile/mcp
返回已准备好进行复制粘贴的MCP连接配置块。
答复(200):
{
"proxy_url": "https://vire-mcp-a1b2c3-xyz123abc-ts.a.run.app",
"claude_code_config": {
"mcpServers": {
"vire": {
"type": "http",
"url": "https://vire-mcp-a1b2c3-xyz123abc-ts.a.run.app/mcp"
}
}
},
"claude_desktop_config": {
"mcpServers": {
"vire": {
"url": "https://vire-mcp-a1b2c3-xyz123abc-ts.a.run.app/mcp"
}
}
}
}GET /api/profile/status
返回用户MCP代理的运行状况和活动状态。
答复(200):
{
"proxy_status": "running",
"last_activity": "2026-02-09T12:30:00Z",
"proxy_url": "https://vire-mcp-a1b2c3-xyz123abc-ts.a.run.app"
}可能的 proxy_status 值: running, stopped, not_provisioned, throttled.
使用路线(需要JWT)
GET /api/usage
返回当前计费周期的使用统计信息。
答复(200):
{
"period": "2026-02",
"total_requests": 3421,
"quota_limit": 10000,
"quota_remaining": 6579,
"status": "active",
"daily_counts": [
{ "date": "2026-02-01", "count": 120 },
{ "date": "2026-02-02", "count": 185 }
],
"top_endpoints": [
{ "endpoint": "portfolio_compliance", "count": 842 },
{ "endpoint": "get_summary", "count": 521 },
{ "endpoint": "compute_indicators", "count": 498 }
]
}计费路线(需要JWT)
POST /api/billing/checkout
创建Stripe Checkout会话以升级到Pro。
答复(200):
{
"checkout_url": "https://checkout.stripe.com/c/pay/..."
}前端将浏览器重定向到 checkout_url 通过 window.location.href = checkout_urlStripe处理付款。完成后,Stripe将重定向回 https://${DOMAIN}/billing?session_id={CHECKOUT_SESSION_ID} --网关在创建会话时配置此返回URL。网关接收Stripe webhook以更新用户的计划。
POST /api/billing/portal
创建条纹计费门户会话,用于管理订阅、查看发票和取消。
答复(200):
{
"portal_url": "https://billing.stripe.com/p/session/..."
}前端将浏览器重定向到 portal_url.
好的,带上你自己的钥匙
用户必须提供自己的API密钥。Vire不代理或转售API访问权限。
关键配置用户体验
配置文件页面显示三个关键字段:
| 服务 | 必需 | 提供什么 | 从哪里获取密钥 |
|---|---|---|---|
| EODHD | 是 | 股票价格、基本面、筛选 | Eodhisions数据网 ->定价->API密钥 |
| 纳维克萨 | 否 | 从您的经纪商同步投资组合 | 航空公司.io ->设置->API |
| 谷歌双子座 | 否 | 人工智能驱动的文件分析、新闻情报 | aistudio.google.com/apikey |
显示状态
每个关键字段显示一个(80年代黑白设计——无颜色,仅文本指示器):
- 未配置 --虚线边框,“未设置”标签,带“添加”按钮的空输入
- 已配置 --实心边界,
[OK] ::::abcd显示最后4个字符,“更新”和“删除”按钮 - 无效 --实心边界,
[ERR]带有验证错误消息的标签
安全
- 密钥使用云KMS加密并存储在GCP密钥管理器中
- 密钥从不存储在Firestore中(配置文件仅包含一个引用)
- 密钥从不被记录或共享
- 输入后,只有最后4个字符可见
- 每个密钥都直接注入到用户的专用MCP代理中,作为Cloud Run秘密参考
Dockerfile
位于 docker/Dockerfile与病毒生态系统模式相匹配的多阶段构建。第一阶段构建Go二进制文件;第二阶段在阿尔卑斯山进行。
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /build
ARG VERSION=dev
ARG BUILD=unknown
ARG GIT_COMMIT=unknown
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w \
-X 'github.com/bobmcallan/vire-portal/internal/config.Version=${VERSION}' \
-X 'github.com/bobmcallan/vire-portal/internal/config.Build=${BUILD}' \
-X 'github.com/bobmcallan/vire-portal/internal/config.GitCommit=${GIT_COMMIT}'" \
-o vire-portal ./cmd/vire-portal
# Runtime stage
FROM alpine:3.21
LABEL org.opencontainers.image.source="https://github.com/bobmcallan/vire-portal"
WORKDIR /app
RUN apk --no-cache add ca-certificates wget
COPY --from=builder /build/vire-portal .
COPY --from=builder /build/pages ./pages
COPY --from=builder /build/docker/vire-portal.toml .
COPY .version .
RUN mkdir -p /app/logs
EXPOSE 8080
HEALTHCHECK NONE
ENTRYPOINT ["./vire-portal"]GitHub操作工作流
测试 (.github/workflows/test.yml):在PR上运行并推送到main。两份工作: unit-test (去兽医+去测试。/内部/…)和 ui-test (无头Chrome浏览器通过 ./scripts/ui-test.sh all).测试工件在失败时上传。
发布 (.github/workflows/release.yml):在推送到主标签或版本标签时,为vire门户和vire mcp构建Docker镜像并将其推送到GHCR(矩阵策略):
- 从中提取版本
.version文件 - 将VERSION、BUILD、GIT_COMMIT作为Docker构建参数传递
- 标签:
latest(上主),语义版本,短SHA - 将GitHub Actions缓存用于Docker层
.version文件
这 .version 项目根目录下的文件是唯一的事实来源:
version: 0.1.2
build: 02-14-20-27-29version:是语义版本build:是上次构建的时间戳,由构建/部署脚本自动更新- 两者
deploy.sh和build.sh将VERSION、BUILD和GIT_COMMIT作为Docker构建参数注入 - CI工作流程(
release.yml)使用相同的版本提取模式
数据层
门户是无状态的,它不在本地存储任何用户数据。所有用户配置文件和设置都由vire-server通过其REST API进行管理:
GET /api/users/{id}--获取用户配置文件(用户名、电子邮件、角色、navexa密钥状态)PUT /api/users/{id}--更新用户字段(例如,navexa键)
API客户端位于 internal/client/vire_client.go.
项目结构
vire-portal/
├── .github/
│ └── workflows/
│ ├── release.yml # Docker build + GHCR push (matrix: portal + mcp)
│ └── test.yml # CI: go vet, unit tests, UI tests on PR/push
├── cmd/
│ ├── vire-portal/
│ │ └── main.go # Portal entry point (flag parsing, config, graceful shutdown)
│ └── vire-mcp/
│ ├── main.go # Stdio-to-HTTP bridge (connects to vire-portal)
│ ├── oauth.go # OAuth 2.1 browser flow + local callback server
│ └── tokenstore.go # File-based OAuth token persistence (~/.vire/credentials.json)
├── internal/
│ ├── app/
│ │ └── app.go # Dependency container (Config, Logger, Handlers, OAuthServer)
│ ├── auth/
│ │ ├── authorize.go # GET /authorize handler (PKCE, session tracking, auto-register)
│ │ ├── dcr.go # POST /register handler (RFC 7591 Dynamic Client Registration)
│ │ ├── discovery.go # .well-known OAuth discovery endpoints
│ │ ├── pkce.go # PKCE S256 verification (constant-time compare)
│ │ ├── server.go # OAuthServer (central state, JWT minting, auth completion)
│ │ ├── session.go # SessionStore for pending MCP auth sessions (TTL 10 min)
│ │ ├── store.go # ClientStore, CodeStore, TokenStore (in-memory, mutex-protected)
│ │ └── token.go # POST /token handler (auth_code + refresh_token grants)
│ ├── config/
│ │ ├── config.go # TOML loading with defaults -> file -> env -> CLI priority
│ │ ├── config_test.go
│ │ ├── defaults.go # Default configuration values
│ │ ├── version.go # Version info (ldflags + .version file)
│ │ └── version_test.go
│ ├── handlers/
│ │ ├── auth.go # OAuth auth handlers (dev login, Google/GitHub redirects, callback, logout, JWT validation)
│ │ ├── auth_test.go # Auth handler tests (ValidateJWT, IsLoggedIn, OAuth flows)
│ │ ├── auth_integration_test.go # Integration tests (full login round-trip, OAuth chains)
│ │ ├── auth_stress_test.go # Security stress tests (alg:none attack, tampering, timing, hostile inputs)
│ │ ├── dashboard.go # GET /dashboard (portfolio management, holdings)
│ │ ├── strategy.go # GET /strategy (portfolio strategy and plan editors)
│ │ ├── mcp_page.go # GET /mcp-info (MCP connection config, tools catalog)
│ │ ├── handlers_test.go
│ │ ├── health.go # GET /api/health
│ │ ├── helpers.go # WriteJSON, RequireMethod, WriteError
│ │ ├── landing.go # PageHandler (template rendering + static file serving)
│ │ ├── profile.go # GET/POST /profile (user info + Navexa API key management)
│ │ └── version.go # GET /api/version
│ ├── cache/
│ │ ├── cache.go # API response cache (TTL, max entries, prefix invalidation)
│ │ └── cache_test.go
│ ├── client/
│ │ ├── vire_client.go # HTTP client for vire-server user API (GetUser, UpdateUser)
│ │ └── vire_client_test.go
│ ├── mcp/
│ │ ├── catalog.go # Dynamic tool catalog types, FetchCatalog, BuildMCPTool, GenericToolHandler
│ │ ├── context.go # UserContext (per-request user identity for proxy headers)
│ │ ├── handler.go # MCP HTTP handler (Streamable HTTP + JWT auth, catalog fetch at startup)
│ │ ├── handler_test.go # Tests: withUserContext, extractJWTSub
│ │ ├── handler_stress_test.go # Stress tests: hostile cookies, concurrent access, binary garbage
│ │ ├── handlers.go # errorResult helper, resolvePortfolio
│ │ ├── mcp_test.go # Tests: catalog, validation, tools, handlers, proxy, integration
│ │ ├── proxy.go # HTTP proxy to vire-server with X-Vire-* headers
│ │ ├── tools.go # RegisterToolsFromCatalog (dynamic registration)
│ │ ├── version.go # Combined get_version handler (vire_portal + vire_server)
│ │ └── version_test.go # Version handler tests
│ ├── server/
│ │ ├── middleware.go # Correlation ID, logging, CORS, recovery
│ │ ├── middleware_test.go
│ │ ├── route_helpers.go # RouteByMethod, RouteResourceCollection
│ │ ├── route_helpers_test.go
│ │ ├── routes.go # Route registration
│ │ ├── routes_test.go
│ │ └── server.go # HTTP server (net/http, timeouts, graceful shutdown)
│ └── vire/ # Shared packages (migrated from vire repo)
│ ├── common/ # Version, logging, config, formatting helpers
│ ├── interfaces/ # Service and storage interface contracts
│ └── models/ # Data structures (portfolio, market, strategy, etc.)
├── tests/
│ ├── common/ # Test utilities (browser, config, containers, screenshot)
│ │ ├── browser.go # chromedp helpers (NewBrowserContext, NavigateAndWait, etc.)
│ │ ├── containers.go # Docker test env (SurrealDB + vire-server + portal via testcontainers)
│ │ ├── testconfig.go # Test config loader (TOML, results dir, timestamps)
│ │ └── screenshot.go # Screenshot capture utility
│ ├── docker/ # Docker configs for test environment
│ │ ├── docker-compose.yml # Full test stack (SurrealDB + vire-server + portal)
│ │ ├── Dockerfile.server # Portal test image (multi-stage build)
│ │ └── portal-test.toml # Portal test configuration
│ └── ui/ # UI browser tests
│ ├── test_config.toml # Test configuration (server URL, browser settings)
│ ├── ui_helpers_test.go # Test helpers (newBrowser, isVisible, etc.)
│ ├── smoke_test.go # Smoke tests (landing, dashboard, branding)
│ ├── dashboard_test.go # Dashboard tests (sections, panels, growth chart, design rules)
│ ├── strategy_test.go # Strategy page tests (editors, nav, portfolio selector)
│ ├── capital_test.go # Capital page tests (cash transactions, pagination, summary)
│ ├── nav_test.go # Navigation tests (hamburger, dropdown, mobile)
│ └── auth_test.go # Auth tests (Google/GitHub login redirects)
├── pages/
│ ├── dashboard.html # Dashboard page (portfolio selector, holdings, capital performance, indicators, growth chart, refresh)
│ ├── strategy.html # Strategy page (portfolio strategy and plan editors)
│ ├── cash.html # Cash page (cash transactions ledger, paged table)
│ ├── mcp.html # MCP info page (connection details, tools table)
│ ├── landing.html # Landing page (Go html/template)
│ ├── profile.html # Profile page (user info + Navexa API key management)
│ ├── partials/
│ │ ├── head.html # HTML head (IBM Plex Mono, Chart.js CDN, Alpine.js CDN)
│ │ ├── nav.html # Navigation bar
│ │ └── footer.html # Footer
│ └── static/
│ ├── css/
│ │ └── portal.css # 80s B&W aesthetic (no border-radius, no box-shadow)
│ └── common.js # Client logging, Alpine.js init, vireStore (fetch cache/dedup), portfolioDashboard() (growth chart), cashTransactions(), portfolioStrategy()
├── docker/
│ ├── Dockerfile # Portal multi-stage build (golang:1.25 -> alpine)
│ ├── Dockerfile.mcp # MCP stdio binary build (golang:1.25 -> alpine)
│ ├── docker-compose.yml # Portal + vire-server stack
│ ├── docker-compose.dev.yml # Dev overlay (VIRE_ENV=dev, used by deploy.sh local)
│ ├── docker-compose.ghcr.yml # GHCR pull + watchtower auto-update
│ ├── vire-portal.toml # Portal configuration
│ └── README.md # Docker usage documentation
├── docs/
│ ├── requirements.md # API contracts and architecture
│ ├── architecture-comparison.md
│ └── authentication/
│ └── mcp-oauth-implementation-steps.md # MCP OAuth implementation plan (Phase 1 complete)
├── scripts/
│ ├── deploy.sh # Deploy orchestration (local/ghcr/down/prune)
│ ├── build.sh # Docker image builder (--portal, --mcp, or both)
│ ├── run.sh # Build + start/stop/restart server locally
│ ├── ui-test.sh # UI test runner (smoke, dashboard, nav, auth, all)
│ ├── verify-auth.sh # Auth endpoint validation (health, login, OAuth, MCP)
│ └── test-scripts.sh # Validation suite for scripts and configs
├── .dockerignore
├── .version # Version metadata (source of truth)
├── go.mod
├── go.sum
├── .gitignore
├── LICENSE
└── README.mdDocker(本地)
该项目包括与以下内容匹配的部署脚本 维尔 项目模式。看 docker/README.md 了解全部细节。
服务栈(门户+病毒服务器)
推荐的部署通过docker compose将门户和vire服务器一起运行:
# Build portal and start services (uses dev compose overlay)
./scripts/deploy.sh local这将开始:
- vire门户 在端口8881上--登录页+MCP端点(开发模式)
- vire服务器 在端口8882上--后端API(从GHCR获取)
本地部署自动使用 docker-compose.dev.yml 作为一个合成叠加,它设置 VIRE_ENV=dev 启用开发模式(开发登录等)。基础 docker-compose.yml 对于类似prod的构建保持不变。
门户通过以下方式连接到vire服务器 VIRE_API_URL=http://vire-server:8080 (Docker内部网络)。克劳德代码连接到 http://localhost:8881/mcp (门户)。Claude Desktop使用 vire-mcp 通过stdio(见上文“连接克劳德”一节)。
仅限门户
# Build and run portal standalone (MCP proxy will fail without vire-server)
./scripts/deploy.sh local
# Build with forced rebuild (no cache)
./scripts/deploy.sh local --force
# Deploy from GHCR with watchtower auto-update
./scripts/deploy.sh ghcr
# Stop all containers
./scripts/deploy.sh down
# Prune stopped containers and dangling images
./scripts/deploy.sh prune或者,构建一个没有docker compose的独立镜像:
# Build Docker image with version injection
./scripts/build.sh
# Build with verbose output
./scripts/build.sh --verbose
# Clean existing images and rebuild
./scripts/build.sh --clean或者直接使用docker:
# Build the Docker image
docker build -f docker/Dockerfile -t vire-portal:latest .
# Run on host port 8881
docker run -p 8881:8080 \
-e VIRE_SERVER_HOST=0.0.0.0 \
-e VIRE_API_URL=http://host.docker.internal:8080 \
vire-portal:latest云运行部署
该门户在Cloud Run上运行,通过vire-infraTerraform部署。Terraform模块位于 infra/modules/portal/main.tf 在vire-infrarepo中。
从Terraform配置
# Simplified from vire-infra/infra/modules/portal/main.tf
resource "google_cloud_run_v2_service" "portal" {
name = "vire-portal"
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
template {
scaling {
min_instance_count = 0
max_instance_count = 3
}
containers {
image = "ghcr.io/bobmcallan/vire-portal:latest"
ports { container_port = 8080 }
env { name = "VIRE_SERVER_HOST"; value = "0.0.0.0" }
env { name = "VIRE_SERVER_PORT"; value = "8080" }
env { name = "VIRE_LOG_LEVEL"; value = "info" }
resources {
limits = { cpu = "1", memory = "256Mi" }
}
}
}
}关键属性:
- 图片:
ghcr.io/bobmcallan/vire-portal:latest(由该仓库的GitHub Actions发布) - 端口: 8080
- 进入: 全部(公共网站)
- 认证: 未经身份验证(通过IAM allUsers公开访问)
- 缩放比例: 0-3个实例(空闲时缩放为零)
- 资源: 1个CPU,256Mi内存
释放
推送版本标签以触发GitHub Actions工作流:
git tag v0.1.2
git push origin v0.1.2这建立并推动 ghcr.io/bobmcallan/vire-portal 带有版本标签和 :latest GHCR。
推到 main 也会触发一个构建 :latest 标签。您可以使用“运行工作流”从“操作”选项卡手动触发构建。
vire infraTerraform参考 ghcr.io/bobmcallan/vire-portal:latest推送新映像后,重新应用Terraform或更新Cloud Run服务以拉取新映像。
体系结构上下文
该门户在Docker Compose堆栈中与vire服务器一起运行。 vire-mcp 是Claude Desktop推出的临时stdio到HTTP网桥:
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ User │ │ Claude Code / │ │ Claude Desktop │
│ (browser) │ │ Claude CLI │ │ │
└───────┬───────┘ └────────┬──────────┘ └────────┬─────────┘
│ GET / │ HTTP + OAuth │ stdio
│ │ │
│ │ ┌────────┴─────────┐
│ │ │ vire-mcp │
│ │ │ - stdio ↔ HTTP │
│ │ │ - OAuth 2.1 │
│ │ │ - Token cache │
│ │ └────────┬─────────┘
│ │ │ HTTP + OAuth
┌────────┴─────────────────────┴────────────────────────┴─────────┐
│ vire-portal (:8881) │
│ - Landing page, dashboard, strategy, profile │
│ - MCP endpoint (Streamable HTTP at POST /mcp) │
│ - OAuth 2.1 (DCR, PKCE, token exchange) │
│ - Dynamic tool catalog from vire-server │
│ - X-Vire-* header injection, JWT auth │
└────────────────────────────┬────────────────────────────────────┘
│ REST API proxy
┌────────────────────────────┴────────────────────────────────────┐
│ vire-server (:8080) │
│ - Portfolio analysis, market data │
│ - Report generation, stock screening │
│ - Strategy and plan management │
└─────────────────────────────────────────────────────────────────┘所有工具逻辑都存在于vire门户中。 vire-mcp 是一个纯传输适配器,它不导入 internal/mcp 或直接联系vire服务器。该门户实现了OAuth 2.1,因此Claude Code和Claude Desktop(通过vire mcp)都对用户进行身份验证,并接收每个用户的JWT令牌。vire-server从中的用户ID解析用户的API密钥(Navexa、EODHD等) X-Vire-User-ID 头球
许可证
私有存储库。
