内容
# lunchmoney-go
Go 客户端和 Lunch Money v2 API 的 MCP 服务器。
- **库**: 导入 `github.com/Cidan/lunchmoney-go` 以访问 v2 API。
- **MCP 服务器**: `go install github.com/Cidan/lunchmoney-go/cmd/lunchmoney-mcp@latest` 通过 stdio 或可流式传输的 HTTP 暴露 API 作为 MCP 工具。
该库通过 `oapi-codegen` 从官方 OpenAPI 规范生成的 DTO 提供惯用的 Go 方法签名。
## 认证模式
客户端在构造时选择 **恰好一种** 认证模式。模式是互斥的 - **没有优先级,没有回退**。每个调用都根据配置的模式验证请求上下文,并在不匹配时返回类型化错误。
| 模式 | 构造函数选项 | 必需的 ctx 值 | 禁止的 ctx 值 |
|---|---|---|---|
| 静态 | `WithStaticToken(s)` | — | `auth.Token`, `auth.TokenID` |
| 上下文 | `WithContextAuth()` | `auth.Token` | `auth.TokenID` |
| OAuth2 有状态 | `WithTokenSource(src)` | — | `auth.Token`, `auth.TokenID` |
| OAuth2 无状态 | `WithTokenStore(cfg, store)` | `auth.TokenID` | `auth.Token` |
错误:
- `ErrNoAuthMode` — `NewClient` 调用时没有认证选项
- `ErrConflictingAuthModes` — `NewClient` 调用时有多个认证选项
- `ErrAuthModeMismatch` — 上下文携带的值与配置的模式不一致
- `ErrMissingContextToken` — ContextAuth 客户端在没有 `auth.WithToken` 的情况下调用
- `ErrMissingContextTokenID` — TokenStore 客户端在没有 `auth.WithTokenID` 的情况下调用
- `ErrOAuth2NotImplemented` — OAuth2 存根的占位符(见下文)
所有错误都支持 `errors.Is`。
### 模式 1: 静态令牌
```go
c, _ := lunchmoney.NewClient(lunchmoney.WithStaticToken(os.Getenv("LM_TOKEN")))
me, err := c.Me.Get(ctx)
```
### 模式 2: 来自 `context.Context` 的令牌
```go
import (
lunchmoney "github.com/Cidan/lunchmoney-go"
"github.com/Cidan/lunchmoney-go/auth"
)
c, _ := lunchmoney.NewClient(lunchmoney.WithContextAuth())
ctx = auth.WithToken(ctx, req.Header.Get("X-LM-Token"))
txns, err := c.Transactions.List(ctx, nil)
```
同一个客户端实例可以并发服务多个租户 - 令牌在每次请求时从 `ctx` 读取。
### 模式 3: OAuth2 有状态(一个客户端 == 一个令牌)
```go
import "github.com/Cidan/lunchmoney-go/oauth2lm"
cfg := &oauth2lm.Config{
ClientID: "abc",
ClientSecret: "shh",
RedirectURL: "https://app.example.com/cb",
Scopes: []string{"read", "write"},
}
src := cfg.TokenSource(ctx, &oauth2lm.Token{AccessToken: "..."})
c, _ := lunchmoney.NewClient(lunchmoney.WithTokenSource(src))
```
### 模式 4: OAuth2 无状态(TokenStore + 上下文中的 ID)
```go
type pgStore struct{ db *sql.DB }
func (s *pgStore) Get(ctx context.Context, id string) (*oauth2lm.Token, error) { /* ... */ }
func (s *pgStore) Put(ctx context.Context, id string, t *oauth2lm.Token) error { /* ... */ } // 刷新写回
cfg := &oauth2lm.Config{ /* ... */ }
c, _ := lunchmoney.NewClient(lunchmoney.WithTokenStore(cfg, &pgStore{db: db}))
ctx = auth.WithTokenID(ctx, currentUserID)
txns, err := c.Transactions.List(ctx, nil)
```
### OAuth2 状态
OAuth2 **被存根**。`oauth2lm` 包存在具有完整的类型和接口表面,以便调用者可以今天连接 OAuth2 模式客户端,但操作助手(`Config.Exchange`、`TokenSource.Token`、`Config.AuthCodeURL`)都返回 `ErrOAuth2NotImplemented`。通过 lunchmoney 客户端本身的连接(模式验证,ctx 检查,存储获取/源调用)是真实的,并通过测试套件进行练习 - 只有凭证发布/刷新逻辑缺失。
## 资源
客户端上的服务(Stripe 风格):
- `c.Me`
- `c.Summary`
- `c.Categories` — `List`, `Get`, `Create`, `Update`, `Delete`
- `c.ManualAccounts` — `List`, `Get`, `Create`, `Update`, `Delete`
- `c.PlaidAccounts` — `List`, `Get`, `Fetch`
- `c.Transactions` — `List`, `Get`, `Create`, `UpdateTransaction`, `BulkUpdate`, `Delete`, `BulkDelete`, `Group`, `Ungroup`, `Split`, `Unsplit`, `AttachFile`, `AttachmentURL`, `DeleteAttachment`
- `c.Tags` — `List`, `Get`, `Create`, `Update`, `Delete`
- `c.RecurringItems` — `List`, `Get`
- `c.Budgets` — `Settings`, `Upsert`, `Delete`
使用 `lunchmoney.MockBaseURL`(或便捷选项 `WithMockServer()`)指向静态模拟服务器,该服务器接受任何 ≥11 个字符的 bearer 令牌。
## MCP 服务器
```sh
# stdio(默认;适用于桌面 MCP 客户端)
LUNCHMONEY_TOKEN=lm_... lunchmoney-mcp
# 显式令牌
lunchmoney-mcp --token lm_...
# 可流式传输的 HTTP,一个共享的静态令牌
lunchmoney-mcp --transport http --addr :8080 --token lm_...
# 可流式传输的 HTTP,每个请求的令牌来自标头
lunchmoney-mcp --transport http --addr :8080 --auth header:X-LM-Token
# 运行针对静态模拟服务器(适用于本地开发)
lunchmoney-mcp --transport stdio --token TESTTOKEN1234 --base-url mock
```
服务器上注册的工具 - **100% v2 端点覆盖**(`docs/lunchmoney.json` 中的每个读取 + 写入端点都有一个匹配的 MCP 工具):
- `lm_me_get`, `lm_summary_get`
- 类别 - `lm_categories_list`, `lm_categories_get`, `lm_categories_create`, `lm_categories_update`, `lm_categories_delete`
- 手动账户 - `lm_manual_accounts_list`, `lm_manual_accounts_get`, `lm_manual_accounts_create`, `lm_manual_accounts_update`, `lm_manual_accounts_delete`
- Plaid 账户 - `lm_plaid_accounts_list`, `lm_plaid_accounts_get`, `lm_plaid_accounts_fetch`
- 标签 - `lm_tags_list`, `lm_tags_get`, `lm_tags_create`, `lm_tags_update`, `lm_tags_delete`
- 循环 - `lm_recurring_list`, `lm_recurring_get`
- 预算 - `lm_budgets_settings_get`, `lm_budgets_upsert`, `lm_budgets_delete`
- 交易 - `lm_transactions_list`, `lm_transactions_get`, `lm_transactions_create`, `lm_transactions_update`, `lm_transactions_delete`, `lm_transactions_bulk_update`, `lm_transactions_bulk_delete`, `lm_transactions_group`, `lm_transactions_ungroup`, `lm_transactions_split`, `lm_transactions_unsplit`, `lm_transactions_attachment_upload`, `lm_transactions_attachment_url`, `lm_transactions_attachment_delete`
编写针对具有 `oneOf` 编码请求字段的端点的工具(手动账户 `balance`,预算/交易 `amount`,拆分子项 `amount`)接受一个自由格式的 `body` 映射,并通过 `Client.RawJSON` 路由;生成的有效载荷仍然通过 `Client.do` 中的身份验证模式验证。工具的正文完全类型化(删除具有选项,批量删除,plaid 获取,附件 URL/删除,取消分组/取消拆分)调用客户端上的相应类型化方法。
附件上传工具(`lm_transactions_attachment_upload`)接受 base64 编码的 `content_b64` 字符串以及 `file_name` 和 `content_type`;服务器解码并作为 `multipart/form-data` 转发。
## 代码生成
生成的 DTO 位于 `internal/lmtypes` 中,由 `oapi-codegen` 从 `docs/lunchmoney.json` 生成,通过 `go.mod` 中的 Go 1.24+ `tool` 指令固定。
要重新生成:
```sh
go generate ./...
```
公共接口在根包中重新导出调用者需要的 DTO(例如 `lunchmoney.Transaction`、`lunchmoney.Category`);生成的包本身是私有的,不能从模块外部导入。
## 测试
```sh
# 单元测试 + MCP 烟雾测试(无网络)
go test ./...
# 针对实时模拟服务器的只读集成测试
go test -tags=integration ./...
# 完整的 CRUD 集成测试(较慢;模拟有每令牌速率限制)
go test -tags=integration_crud -p 1 ./...
```
集成套件使用 bearer 令牌 `TESTTOKEN1234`(任何 ≥11 个字符的字符串都适用于 `https://mock.lunchmoney.dev/v2` 的模拟)。`integration_crud` 套件添加了 Create / Update / Delete 覆盖,验证请求有效载荷形状与模拟的 OpenAPI 验证器相匹配。它单独被禁用,因为模拟强制执行令牌桶速率限制(~10 请求/40 秒),而 CRUD 套件发出足够的调用,在争用下会失败;单独使用 `-p 1` 运行它使其可靠。
> **注意**:一些类型化的 Create 方法(`ManualAccounts.Create`、`Transactions.Create`、`Budgets.Upsert`)包含其模式使用 `oneOf` 的字段 - `oapi-codegen` 将这些字段包装在一个具有私有联合的结构中,因此无法从公共接口填充它们。目前,通过 `Client.RawJSON` 路由这些创建(集成套件演示);未来的传递可以添加方便的构造函数帮助器。
## 许可证
Apache 2.0(与上游 OpenAPI 规范匹配)。