fix(tasks): add assignee filtering to list_all_uncompleted#58
fix(tasks): add assignee filtering to list_all_uncompleted#58devondragon merged 3 commits intomainfrom
Conversation
The list_all_uncompleted operation silently ignored assignee/assigneeId params, so "show me my tasks" queries returned everyone's tasks. - Add assigneeId and assignee fields to ListAllUncompletedParams - Add assignee resolution (including 'me' shortcut) to handleListAllUncompleted, matching the pattern in handleList - Pass assigneeId through getAllUncompletedTasks to per-workspace getTasks calls for server-side filtering - Update tool descriptions to indicate assignee params work with both list and list_all_uncompleted operations
The quiet option does not exist in dotenv's DotenvConfigOptions type. dotenv.config() already returns errors without throwing when .env is missing, so the option was unnecessary.
Code ReviewSummaryThis PR correctly fixes a real usability bug (assignee filtering silently ignored on Issues1. Code Duplication — Medium severityThe assignee resolution block in // Duplicated in both handleList and handleListAllUncompleted:
const normalizeDisplayFromUser = (user: { name?: string; email?: string; id: string }) => {
return user.name || user.email || user.id;
};
const resolveCurrentUser = async () => {
const currentUser = await this.motionService.getCurrentUser();
resolvedAssigneeId = currentUser.id;
assigneeDisplay = normalizeDisplayFromUser(currentUser);
};This violates DRY and means future changes to the resolution logic need to be applied in two places. Both helpers (and the ~20 lines of if/else branching that follow) should be extracted to private methods on private normalizeDisplayFromUser(user: { name?: string; email?: string; id: string }): string {
return user.name || user.email || user.id;
}
private async resolveAssigneeParams(
assigneeId?: string,
assignee?: string,
workspaceIds?: string[]
): Promise<{ resolvedAssigneeId?: string; assigneeDisplay?: string }> {
// shared resolution logic
}2. Mutation via closure — Low severityThe 3.
|
There was a problem hiding this comment.
Pull request overview
This PR adds assignee filtering support to the list_all_uncompleted operation and fixes a TypeScript build error related to dotenv configuration. The operation previously did not support assignee filtering, which meant cross-workspace queries returned all users' uncompleted tasks. The fix brings feature parity with the list operation by implementing the same assignee resolution logic, including support for the 'me' shortcut and name-based user lookup across workspaces.
Changes:
- Add assignee filtering to
list_all_uncompletedoperation with support for assigneeId and assignee parameters, including 'me' shortcut and cross-workspace name resolution - Fix TypeScript build error by removing invalid
quietoption from dotenv.config() call - Update parameter descriptions in tool definitions to reflect that assignee filtering works with both
listandlist_all_uncompletedoperations
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/handlers/TaskHandler.ts | Added assigneeId/assignee parameters to ListAllUncompletedParams interface; implemented assignee resolution logic with 'me' shortcut and cross-workspace user lookup; passed resolved assignee to API calls and response formatter |
| src/services/motionApi.ts | Added optional assigneeId parameter to getAllUncompletedTasks method and passed it through to per-workspace getTasks calls |
| src/tools/ToolDefinitions.ts | Updated assigneeId and assignee parameter descriptions to indicate they work with both list and list_all_uncompleted operations |
| src/mcp-server.ts | Removed invalid { quiet: true } option from dotenv.config() call to fix TypeScript build error |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private async handleListAllUncompleted(params: ListAllUncompletedParams): Promise<McpToolResponse> { | ||
| const tasks = await this.motionService.getAllUncompletedTasks(params.limit); | ||
| let resolvedAssigneeId = params.assigneeId; | ||
| let assigneeDisplay: string | undefined = params.assignee; | ||
|
|
||
| const normalizeDisplayFromUser = (user: { name?: string; email?: string; id: string }) => { | ||
| return user.name || user.email || user.id; | ||
| }; | ||
|
|
||
| const resolveCurrentUser = async () => { | ||
| const currentUser = await this.motionService.getCurrentUser(); | ||
| resolvedAssigneeId = currentUser.id; | ||
| assigneeDisplay = normalizeDisplayFromUser(currentUser); | ||
| }; | ||
|
|
||
| if (resolvedAssigneeId) { | ||
| if (resolvedAssigneeId.toLowerCase() === 'me') { | ||
| await resolveCurrentUser(); | ||
| } | ||
| } else if (params.assignee) { | ||
| const assigneeInput = params.assignee.trim(); | ||
| if (assigneeInput.toLowerCase() === 'me') { | ||
| await resolveCurrentUser(); | ||
| } else { | ||
| // For cross-workspace lookup, resolve against all workspaces | ||
| const workspaces = await this.motionService.getWorkspaces(); | ||
| let resolvedUser: { id: string; name?: string; email?: string } | undefined; | ||
| for (const ws of workspaces) { | ||
| resolvedUser = await this.motionService.resolveUserIdentifier({ userName: assigneeInput }, ws.id); | ||
| if (resolvedUser) break; | ||
| } | ||
| if (!resolvedUser) { | ||
| return this.handleError(new Error(`Assignee "${assigneeInput}" not found in any workspace`)); | ||
| } | ||
| resolvedAssigneeId = resolvedUser.id; | ||
| assigneeDisplay = normalizeDisplayFromUser(resolvedUser); | ||
| } | ||
| } |
There was a problem hiding this comment.
The new assignee filtering functionality for list_all_uncompleted lacks test coverage. Consider adding tests that verify: (1) assignee filtering with 'me' shortcut works correctly, (2) assignee name resolution across workspaces functions as expected, (3) the assigneeId parameter is properly passed to getAllUncompletedTasks, and (4) appropriate error handling when an assignee is not found. The existing tests in handlers.task.spec.ts provide examples of how to structure these tests.
| const normalizeDisplayFromUser = (user: { name?: string; email?: string; id: string }) => { | ||
| return user.name || user.email || user.id; | ||
| }; | ||
|
|
||
| const resolveCurrentUser = async () => { | ||
| const currentUser = await this.motionService.getCurrentUser(); | ||
| resolvedAssigneeId = currentUser.id; | ||
| assigneeDisplay = normalizeDisplayFromUser(currentUser); | ||
| }; | ||
|
|
||
| if (resolvedAssigneeId) { | ||
| if (resolvedAssigneeId.toLowerCase() === 'me') { | ||
| await resolveCurrentUser(); | ||
| } | ||
| } else if (params.assignee) { | ||
| const assigneeInput = params.assignee.trim(); | ||
| if (assigneeInput.toLowerCase() === 'me') { | ||
| await resolveCurrentUser(); | ||
| } else { | ||
| // For cross-workspace lookup, resolve against all workspaces | ||
| const workspaces = await this.motionService.getWorkspaces(); | ||
| let resolvedUser: { id: string; name?: string; email?: string } | undefined; | ||
| for (const ws of workspaces) { | ||
| resolvedUser = await this.motionService.resolveUserIdentifier({ userName: assigneeInput }, ws.id); | ||
| if (resolvedUser) break; | ||
| } | ||
| if (!resolvedUser) { | ||
| return this.handleError(new Error(`Assignee "${assigneeInput}" not found in any workspace`)); | ||
| } | ||
| resolvedAssigneeId = resolvedUser.id; | ||
| assigneeDisplay = normalizeDisplayFromUser(resolvedUser); | ||
| } |
There was a problem hiding this comment.
This assignee resolution logic is duplicated from the handleList method (lines 268-293). Consider extracting this into a reusable private method to improve maintainability. The only difference is that handleListAllUncompleted iterates through all workspaces for user lookup (lines 461-466), while handleList uses a single workspace. This could be parameterized with an optional workspaceId parameter.
Extract duplicated assignee resolution logic from handleList and handleListAllUncompleted into a reusable resolveAssignee() method. - Returns values instead of mutating outer variables via closure - Accepts optional workspaceId to support both single-workspace and cross-workspace name lookups - Handles 'me' shortcut, name/email lookup, and direct ID passthrough
fdd0234 to
0880f2a
Compare
PR Review: fix(tasks): add assignee filtering to list_all_uncompletedOverall: This is a clean, well-focused fix that resolves a genuine usability gap and opportunistically reduces code duplication. The changes are appropriately scoped. A few observations below. Positive
Issues / Suggestions1. Sequential cross-workspace user lookup (performance) In const workspaces = await this.motionService.getWorkspaces();
for (const ws of workspaces) {
const user = await this.motionService.resolveUserIdentifier({ userName: input }, ws.id);
if (user) { return ...; }
}
const workspaces = await this.motionService.getWorkspaces();
const results = await Promise.all(
workspaces.map(ws => this.motionService.resolveUserIdentifier({ userName: input }, ws.id))
);
const user = results.find(Boolean);Whether the service supports parallel calls (rate-limiting, connection pool) is a consideration, but it's worth noting. 2. if (assigneeId) {
// ... 'me' case omitted ...
return { resolvedId: assigneeId, display: assignee }; // assignee may be undefined
}If a caller passes only assigneeName: display || resolvedId,This is functionally correct, but the response will show a UUID rather than a human-readable name. A small improvement would be to fetch the user and return their name, or at minimum document this limitation in the JSDoc. 3. No parameter-level validation in The Minor
Summary: Approve with the performance note on sequential lookups as the main thing to consider in a follow-up. The |
Summary
list_all_uncompleted: The operation previously silently ignoredassignee/assigneeIdparams, so cross-workspace queries like "show me my tasks" returned everyone's uncompleted tasks. Now supports the sameassignee: 'me'shortcut and name resolution as thelistoperation, with server-side filtering via the Motion API.quietoption fromdotenv.config()call inmcp-server.ts(not a validDotenvConfigOptionsproperty).Closes #59
Changes
src/handlers/TaskHandler.ts— AddassigneeId/assigneetoListAllUncompletedParams; add assignee resolution logic tohandleListAllUncompleted(including'me'shortcut and cross-workspace name lookup); pass resolved assignee to API and response formattersrc/services/motionApi.ts— Add optionalassigneeIdparam togetAllUncompletedTasksand pass it through to per-workspacegetTasks()callssrc/tools/ToolDefinitions.ts— UpdateassigneeId/assigneeparameter descriptions to indicate they work with bothlistandlist_all_uncompletedsrc/mcp-server.ts— Remove invalid{ quiet: true }fromdotenv.config()Test plan
npm run buildcompiles with zero errorstimeout 3s npm run mcpstarts cleanlymotion_taskswithoperation: 'list_all_uncompleted'andassignee: 'me'returns only the current user's uncompleted tasksmotion_taskswithoperation: 'list_all_uncompleted'without assignee params still returns all uncompleted tasks (no regression)