feat(mcp): granular OAuth scopes and per-client rate limiting

- Split `media:read` into `geo:read` and `weather:read` scopes
- Add dedicated `atlas:read/write` scopes (previously under `places`)
- Add dedicated `todos:read/write` scopes (previously under `collab`)
- Rate limiting now keyed by userId+clientId instead of userId alone
- Bind MCP sessions to the OAuth client that created them
- Log MCP tool calls to audit log with clientId
- Invalidate all MCP sessions on addon state change
- Reduce session sweep interval from 10min to 1min
- Update all translations with new scope labels
This commit is contained in:
jubnl
2026-04-11 02:06:09 +02:00
parent 4670d4914c
commit 535c06bb3f
39 changed files with 1930 additions and 237 deletions
+4
View File
@@ -111,6 +111,8 @@ export function registerAssignmentTools(server: McpServer, userId: number, scope
async ({ tripId, assignmentId, newDayId, oldDayId, orderIndex }) => {
if (isDemoUser(userId)) return demoDenied();
if (!canAccessTrip(tripId, userId)) return noAccess();
if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true };
if (!getDay(newDayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
const result = moveAssignment(assignmentId, newDayId, orderIndex ?? 0, oldDayId);
safeBroadcast(tripId, 'assignment:moved', { assignment: result.assignment, oldDayId: result.oldDayId });
return ok({ assignment: result.assignment });
@@ -129,6 +131,7 @@ export function registerAssignmentTools(server: McpServer, userId: number, scope
},
async ({ tripId, assignmentId }) => {
if (!canAccessTrip(tripId, userId)) return noAccess();
if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true };
const participants = getAssignmentParticipants(assignmentId);
return ok({ participants });
}
@@ -148,6 +151,7 @@ export function registerAssignmentTools(server: McpServer, userId: number, scope
async ({ tripId, assignmentId, userIds }) => {
if (isDemoUser(userId)) return demoDenied();
if (!canAccessTrip(tripId, userId)) return noAccess();
if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true };
const participants = setAssignmentParticipants(assignmentId, userIds);
safeBroadcast(tripId, 'assignment:participants', { assignmentId, participants });
return ok({ participants });