feat: add mcp support - #310
Conversation
…smart positioning)
…ogging - Add @modelcontextprotocol/sdk dependency - Create MCP database tables (api_keys, tokens, device_codes, audit_logs, settings) - Implement API Key and OAuth token verification - Add scope-based authorization checks - Set up audit logging for MCP operations - Add MCP settings management
- Add .well-known/oauth-authorization-server metadata endpoint - Implement device code request endpoint (/api/oauth/device) - Implement token exchange/polling endpoint (/api/oauth/token) - Create OAuth authorize UI page with user consent flow - Add authorization confirmation API endpoint
- Create McpServer with 16 tools across 5 categories - Implement event CRUD tools with search and pagination - Add category, countdown, settings, and profile tools - Create auth-aware MCP handler with rate limit check - Wire /api/mcp endpoint to handle iJSON-RPC requests - Integrate scope-based authorization in every tool
- API Key CRUD (list, create, revoke, update scopes) - MCP settings toggle (enable/disable, rate limit) - OAuth authorized apps list and revoke - Audit log retrieval with pagination - All routes use existing session-based auth
- Create MCPSettings component with tabbed interface - Implement Overview tab with MCP toggle and endpoint info - Add API Key management (create, list, revoke, edit scopes) - Add OAuth authorized apps viewer with revoke capability - Add audit log viewer with pagination - Integrate MCP section into main settings page
- Implement in-memory rate limiter per user (configurable RPM) - Integrate rate limit check into MCP request handler - Add audit logging for both success and failure MCP requests - Return 429 with retry_after when rate limit exceeded
- Remove unused getClientId function - Clean up catch blocks without error variable usage - Remove unused router import in OAuth authorize page - Add Bot icon instead of unused user profile image - Fix unnecessary spread fallback in settings-tools
- Transport cannot be reused in stateless JSON mode - Create new transport + server per request - Add allowedOrigins: ['*'] for cross-origin MCP clients
…ompatibility - Add mcp_auth_requests table for PKCE auth request storage - Rewrite /oauth/authorize to handle both device_code and auth_code flows - Implement PKCE code challenge verification in token endpoint - Update .well-known to advertise authorization_code grant - Use Node.js crypto for PKCE S256 challenge generation
…ze/device endpoints
…dit and rate limiter - Fix listCategories/listCountdowns/create/update returning encrypted fields - Add pagination to listCountdowns matching spec (50/page) - Remove duplicated HEX_TO_BG color maps, pass through hex colors directly - Extract respond/respondError/respondMessage helpers, eliminate 16x try-catch boilerplate - Remove dead hasScope/requireScope in types.ts - Rename getFullUserInfo -> getUserNameAndEmail - Fix API key prefix display to spec (zc_ + last 4 chars) - Add per-tool action audit logging and OAuth discovery fields in 401 - Replace in-memory rate limiter Map with DB-backed query
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's Guide为日历应用添加完整的 MCP(Model Context Protocol,模型上下文协议)支持,包括 OAuth/设备码流程、API Key、审计/速率限制基础设施、基于 Redis 的会话与事件缓存,以及用于管理 MCP 设置和凭据的 UI,同时重构部分日历视图/弹出层并收紧鉴权重定向。 新增 MCP 表的实体关系图erDiagram
user {
text id PK
}
mcpApiKeys {
text id PK
text userId FK
text name
text keyHash
text keyPrefix
jsonb scopes
boolean isActive
}
mcpTokens {
text id PK
text userId FK
text tokenHash
jsonb scopes
text clientId
text clientName
timestamptz expiresAt
boolean isRevoked
}
mcpDeviceCodes {
text id PK
text userId FK
text deviceCode
text userCode
text clientId
jsonb scopes
text status
}
mcpAuditLogs {
text id PK
text userId FK
text authType
text keyId
text action
boolean success
timestamptz createdAt
}
mcpSettings {
text userId PK,FK
boolean enabled
int rateLimitRpm
}
mcpAuthRequests {
text id PK
text userId FK
text clientId
text redirectUri
jsonb scopes
text authorizationCode
timestamptz codeExpiresAt
text status
}
user ||--o{ mcpApiKeys : has
user ||--o{ mcpTokens : has
user ||--o{ mcpDeviceCodes : has
user ||--o{ mcpAuditLogs : has
user ||--|| mcpSettings : has
user ||--o{ mcpAuthRequests : has
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your Experience打开你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideAdds full MCP (Model Context Protocol) support for the calendar app, including OAuth/device-code flows, API keys, audit/rate-limit infrastructure, Redis-backed caching for sessions and events, and UI to manage MCP settings and credentials, while refactoring some calendar views/popovers and tightening auth redirects. Entity-relationship diagram for new MCP tableserDiagram
user {
text id PK
}
mcpApiKeys {
text id PK
text userId FK
text name
text keyHash
text keyPrefix
jsonb scopes
boolean isActive
}
mcpTokens {
text id PK
text userId FK
text tokenHash
jsonb scopes
text clientId
text clientName
timestamptz expiresAt
boolean isRevoked
}
mcpDeviceCodes {
text id PK
text userId FK
text deviceCode
text userCode
text clientId
jsonb scopes
text status
}
mcpAuditLogs {
text id PK
text userId FK
text authType
text keyId
text action
boolean success
timestamptz createdAt
}
mcpSettings {
text userId PK,FK
boolean enabled
int rateLimitRpm
}
mcpAuthRequests {
text id PK
text userId FK
text clientId
text redirectUri
jsonb scopes
text authorizationCode
timestamptz codeExpiresAt
text status
}
user ||--o{ mcpApiKeys : has
user ||--o{ mcpTokens : has
user ||--o{ mcpDeviceCodes : has
user ||--o{ mcpAuditLogs : has
user ||--|| mcpSettings : has
user ||--o{ mcpAuthRequests : has
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并留下了一些总体反馈:
- 在 events 的 GET 处理器中,你在最终的内存过滤里重新计算了
new Date(startDate)/new Date(endDate);在过滤之前只计算一次并复用这些值,会更简洁也稍微更高效。 lib/cache/client.ts中的 Redis 客户端假定process.env.REDIS_URL一定已定义(使用了!断言);建议增加防御性检查或回退行为,这样在 Redis 未配置时应用不会在启动阶段直接抛错。
提供给 AI Agents 的提示
Please address the comments from this code review:
## Overall Comments
- 在 events 的 GET 处理器中,你在最终的内存过滤里重新计算了 `new Date(startDate)` / `new Date(endDate)`;在过滤之前只计算一次并复用这些值,会更简洁也稍微更高效。
- `lib/cache/client.ts` 中的 Redis 客户端假定 `process.env.REDIS_URL` 一定已定义(使用了 `!` 断言);建议增加防御性检查或回退行为,这样在 Redis 未配置时应用不会在启动阶段直接抛错。
## Individual Comments
### Comment 1
<location path="apps/calendar/lib/cache/events.ts" line_range="9-11" />
<code_context>
+
+export type CachedEvent = typeof calendarEvents.$inferSelect
+
+function parseCachedEvents(json: string): CachedEvent[] {
+ return JSON.parse(json, (_key, value) => {
+ if (
+ typeof value === 'string' &&
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 在 JSON 解析期间进行日期复原,可能会意外地把任意 ISO 风格的字符串字段转换成 Date。
这个 reviver 会应用到任何匹配 `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/` 的字符串,而不考虑属性名,因此任何新的 ISO 风格字符串字段(例如 payload、ID、日志)都会被悄悄转成 `Date`。建议通过检查 `key` 来限制复原范围(只转换已知的日期字段,如 `startDate` / `endDate` / `createdAt` / `updatedAt`),或者利用 `calendarEvents.$inferSelect` 的类型结构,显式控制哪些字段应当被视为日期。
Suggested implementation:
```typescript
export type CachedEvent = typeof calendarEvents.$inferSelect
const DATE_FIELD_KEYS = new Set<string>([
'startDate',
'endDate',
'createdAt',
'updatedAt',
])
function parseCachedEvents(json: string): CachedEvent[] {
return JSON.parse(json, (key, value) => {
if (
typeof value === 'string' &&
DATE_FIELD_KEYS.has(key) &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
) {
return new Date(value)
}
return value
}) as CachedEvent[]
}
```
- 如果 `CachedEvent` 还有其他类似日期的字段(例如 `deletedAt`、`reminderAt`),请把它们的属性名加入 `DATE_FIELD_KEYS`,以确保这些字段会被复原为 `Date` 实例。
- 如果实际的字段名不同(例如 `startsAt` / `endsAt`),请调整 `DATE_FIELD_KEYS` 中的条目,使其与 `calendarEvents.$inferSelect` 的结构保持一致。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的代码评审。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- In the events GET handler, you recompute
new Date(startDate)/new Date(endDate)inside the final in-memory filter; it would be cleaner and slightly more efficient to compute these once before filtering and reuse them. - The Redis client in
lib/cache/client.tsassumesprocess.env.REDIS_URLis always defined (!assertion); consider adding a defensive check or fallback behavior so the app doesn’t throw at startup when Redis is not configured.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In the events GET handler, you recompute `new Date(startDate)`/`new Date(endDate)` inside the final in-memory filter; it would be cleaner and slightly more efficient to compute these once before filtering and reuse them.
- The Redis client in `lib/cache/client.ts` assumes `process.env.REDIS_URL` is always defined (`!` assertion); consider adding a defensive check or fallback behavior so the app doesn’t throw at startup when Redis is not configured.
## Individual Comments
### Comment 1
<location path="apps/calendar/lib/cache/events.ts" line_range="9-11" />
<code_context>
+
+export type CachedEvent = typeof calendarEvents.$inferSelect
+
+function parseCachedEvents(json: string): CachedEvent[] {
+ return JSON.parse(json, (_key, value) => {
+ if (
+ typeof value === 'string' &&
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Date revival during JSON parse can accidentally convert any ISO-like string field to Date.
This reviver applies to any string matching `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/`, regardless of the property, so any new ISO‑like string field (e.g. payloads, IDs, logs) will be silently turned into a `Date`. Consider restricting revival by inspecting the `key` (only converting known date fields like `startDate`/`endDate`/`createdAt`/`updatedAt`) or using the typed shape of `calendarEvents.$inferSelect` to explicitly control which fields are treated as dates.
Suggested implementation:
```typescript
export type CachedEvent = typeof calendarEvents.$inferSelect
const DATE_FIELD_KEYS = new Set<string>([
'startDate',
'endDate',
'createdAt',
'updatedAt',
])
function parseCachedEvents(json: string): CachedEvent[] {
return JSON.parse(json, (key, value) => {
if (
typeof value === 'string' &&
DATE_FIELD_KEYS.has(key) &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
) {
return new Date(value)
}
return value
}) as CachedEvent[]
}
```
- If `CachedEvent` has other date-like fields (e.g. `deletedAt`, `reminderAt`), add their property names to `DATE_FIELD_KEYS` to ensure they are revived as `Date` instances.
- If the actual field names differ (for example `startsAt`/`endsAt`), adjust the entries in `DATE_FIELD_KEYS` to match the schema of `calendarEvents.$inferSelect`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| function parseCachedEvents(json: string): CachedEvent[] { | ||
| return JSON.parse(json, (_key, value) => { | ||
| if ( |
There was a problem hiding this comment.
suggestion (bug_risk): 在 JSON 解析期间进行日期复原,可能会意外地把任意 ISO 风格的字符串字段转换成 Date。
这个 reviver 会应用到任何匹配 /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ 的字符串,而不考虑属性名,因此任何新的 ISO 风格字符串字段(例如 payload、ID、日志)都会被悄悄转成 Date。建议通过检查 key 来限制复原范围(只转换已知的日期字段,如 startDate / endDate / createdAt / updatedAt),或者利用 calendarEvents.$inferSelect 的类型结构,显式控制哪些字段应当被视为日期。
Suggested implementation:
export type CachedEvent = typeof calendarEvents.$inferSelect
const DATE_FIELD_KEYS = new Set<string>([
'startDate',
'endDate',
'createdAt',
'updatedAt',
])
function parseCachedEvents(json: string): CachedEvent[] {
return JSON.parse(json, (key, value) => {
if (
typeof value === 'string' &&
DATE_FIELD_KEYS.has(key) &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
) {
return new Date(value)
}
return value
}) as CachedEvent[]
}- 如果
CachedEvent还有其他类似日期的字段(例如deletedAt、reminderAt),请把它们的属性名加入DATE_FIELD_KEYS,以确保这些字段会被复原为Date实例。 - 如果实际的字段名不同(例如
startsAt/endsAt),请调整DATE_FIELD_KEYS中的条目,使其与calendarEvents.$inferSelect的结构保持一致。
Original comment in English
suggestion (bug_risk): Date revival during JSON parse can accidentally convert any ISO-like string field to Date.
This reviver applies to any string matching /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, regardless of the property, so any new ISO‑like string field (e.g. payloads, IDs, logs) will be silently turned into a Date. Consider restricting revival by inspecting the key (only converting known date fields like startDate/endDate/createdAt/updatedAt) or using the typed shape of calendarEvents.$inferSelect to explicitly control which fields are treated as dates.
Suggested implementation:
export type CachedEvent = typeof calendarEvents.$inferSelect
const DATE_FIELD_KEYS = new Set<string>([
'startDate',
'endDate',
'createdAt',
'updatedAt',
])
function parseCachedEvents(json: string): CachedEvent[] {
return JSON.parse(json, (key, value) => {
if (
typeof value === 'string' &&
DATE_FIELD_KEYS.has(key) &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
) {
return new Date(value)
}
return value
}) as CachedEvent[]
}- If
CachedEventhas other date-like fields (e.g.deletedAt,reminderAt), add their property names toDATE_FIELD_KEYSto ensure they are revived asDateinstances. - If the actual field names differ (for example
startsAt/endsAt), adjust the entries inDATE_FIELD_KEYSto match the schema ofcalendarEvents.$inferSelect.
Implement RFC 7591 registration endpoint so OAuth clients like opencode can register dynamically. Add mcp_oauth_clients table, refresh_token grant support, and oauth-protected-resource metadata for scopes.
Description
Please provide a clear and concise description of what this pull request does.
Type of Change
Areas Affected
Check all that apply:
Testing Done
List all types of testing you've completed:
Security Considerations
For changes involving authentication, data handling, or permissions:
Checklist
Additional Notes
Include any additional context, TODOs, deployment notes, or follow-up tasks.
Screenshots/Recordings
Include screenshots or recordings if this PR changes UI.
By submitting this pull request, I confirm that my contribution is made under the terms of the project's license and coding guidelines.
Summary by Sourcery
添加基于 MCP 的 AI 集成,用于访问日历数据,并引入 OAuth 流程、API 密钥、服务器工具与缓存优化,同时改进日历 UI 行为。
New Features:
Bug Fixes:
Enhancements:
Original summary in English
Summary by Sourcery
Add MCP-based AI integration for calendar data with OAuth flows, API keys, server tooling, and caching improvements while refining calendar UI behavior.
New Features:
Bug Fixes:
Enhancements: