Skip to content

feat: add project CRUD tools - #39

Open
madisonrickert wants to merge 6 commits into
verygoodplugins:mainfrom
madisonrickert:feat/project-crud
Open

feat: add project CRUD tools#39
madisonrickert wants to merge 6 commits into
verygoodplugins:mainfrom
madisonrickert:feat/project-crud

Conversation

@madisonrickert

@madisonrickert madisonrickert commented May 2, 2026

Copy link
Copy Markdown
Contributor

Adds full project CRUD (create, update, delete) plus a small ergonomic refactor of getProject/getProjects. Builds on the same coordination story as #37 and #38.

Note

Builds on #38. The request() 200/empty-body fix is cherry-picked from #38 because project DELETE shares the same Toggl response shape as tag DELETE and time entry DELETE — patch-id collision will let the duplicate commit drop cleanly when either PR rebases. Happy to merge in any order or rebase against post-#38 main — your call.

What's added

  • toggl_create_projectPOST /workspaces/{wid}/projects. Required: name. Optional: client_id, is_private, active, color, billable, auto_estimates, estimated_hours. Defaults active: true and is_private: false to match Toggl's documented defaults.
  • toggl_update_projectPUT /workspaces/{wid}/projects/{pid}. All fields optional except project_id; only fields the caller passed are sent. Set active: false to archive; pass client_id: null to detach the client (the schema declares client_id: ['number', 'null'] and the runtime preserves the explicit null).
  • toggl_delete_projectDELETE /workspaces/{wid}/projects/{pid}. Surfaces Toggl's optional ?teDeletionMode=delete|unassign as a time_entry_deletion_mode parameter so callers can choose between deleting the project's time entries or detaching them.

All three resolve workspace via resolveWorkspaceForTool, return via jsonResponse, and call cache.invalidateWorkspaceProjects(workspaceId) after each write so a subsequent toggl_list_projects reflects the change without a manual toggl_clear_cache.

Small ergonomic refactor along for the ride

  • getProject(projectId, workspaceId?) — when the workspace is known, hits GET /workspaces/{wid}/projects/{pid} directly instead of fanning out across all workspaces. Falls back to the old per-workspace probe when only a project ID is supplied. Non-breaking: existing getProject(projectId) callers still work.
  • getProjects(workspaceId, active?) — accepts 'true' | 'false' | 'both' and maps to Toggl's ?active query param. Optional, defaults to Toggl's default.

Both originally lifted from 84emllc/mcp-toggl@eef1bea and credited via Co-Authored-By.

Includes the empty-body fix (cherry-picked from #38)

Project DELETE returns 200/content-length: 0 like tag and time entry DELETE. Same fix, same patch-id collision story.

Live-test caveat: 402 on billable: true

Toggl returns HTTP 402 when the workspace plan can't apply the billable flag on a project. The existing request() code surfaces all 402s as TOGGL_QUOTA_LIMIT, which is technically misleading (it's a feature-gate, not a rate limit). Pre-existing behavior; not in scope here. Confirmed via live smoke testing that immediately following a non-billable update succeeds, ruling out a real rate limit. All other update fields work cleanly on Free plans.

Commits

  1. fix(api): handle Toggl 200/empty-body responses — cherry-picked from feat: add time entry CRUD tools (closes #7, revisits #8 and #12) #38
  2. refactor(api): allow direct workspace lookup in getProject — Co-Authored-By: Andrew Miller
  3. feat(api): add project CRUD methods to TogglAPI client — Co-Authored-By: Andrew Miller
  4. feat(cache): add workspace project invalidation
  5. feat(tools): expose project CRUD as MCP tools
  6. docs: document project CRUD tools in README

Verification

  • npm test — 38/38 passing (7 new API-level tests: GET active filter, GET direct lookup, POST/PUT/DELETE shape, teDeletionMode passthrough, 4xx no-retry)
  • npm run build — clean
  • npm run lint — no new warnings
  • Smoke-tested end-to-end against a real Toggl workspace: create (with color) → update (rename) → update (archive via active: false) → delete. Cache invalidation verified — subsequent toggl_list_projects reflects each change.

Scope

Project CRUD only. Sister PR for client CRUD is at #40. Archive/restore endpoints, color presets, and bulk operations are out of scope.

madisonrickert and others added 6 commits May 2, 2026 12:38
Toggl returns HTTP 200 with content-length: 0 (not 204) on some write
endpoints, including DELETE /workspaces/{wid}/tags/{tid} and
DELETE /workspaces/{wid}/time_entries/{tid}, per the official Toggl
Engineering API docs. The existing request() short-circuited only on
204, so it called response.json() on the empty body, which threw and
triggered the retry loop without noRetry. The retried call then got a
real 404 because the first call had already succeeded server-side,
surfacing the misleading "Tag was not found" / "not found" errors to
callers despite the operation having completed.

Treat 200 with content-length: 0 (or empty body when Content-Length is
absent) the same as 204. Behavior unchanged for non-empty success
responses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getProject now accepts an optional workspace_id and uses the direct
GET /workspaces/{wid}/projects/{pid} endpoint, avoiding the workspace
fan-out fallback when the caller already knows the workspace. The
workspace-loop fallback remains for callers that pass only a project ID.

getProjects gains an optional active filter ('true' | 'false' | 'both')
mapping to Toggl's ?active query param.

Both improvements lifted from 84emllc/mcp-toggl@eef1bea.

Co-Authored-By: Andrew Miller <andrew@84em.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds createProject, updateProject, and deleteProject methods plus the
backing CreateProjectRequest, UpdateProjectRequest, and ProjectDeleteMode
type definitions. Defaults active=true and is_private=false on creation
to match Toggl's documented defaults. deleteProject accepts an optional
teDeletionMode controlling whether existing time entries are deleted or
unassigned.

Create/update lifted from 84emllc/mcp-toggl@eef1bea; delete is added on
top to round out the CRUD surface.

Co-Authored-By: Andrew Miller <andrew@84em.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds invalidateWorkspaceProjects so callers can drop the cached project
list and per-project entries for a workspace after a write. Mirrors
invalidateWorkspaceTags from the tag CRUD work in PR verygoodplugins#37.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds toggl_create_project, toggl_update_project, and toggl_delete_project
tool definitions and handlers. Each handler resolves the workspace, calls
the matching TogglAPI method, and invalidates cached projects for the
workspace so subsequent toggl_list_projects reflects the change.

toggl_delete_project surfaces Toggl's optional teDeletionMode query
parameter as 'time_entry_deletion_mode' so callers can choose between
deleting the project's time entries or detaching them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@madisonrickert
madisonrickert marked this pull request as ready for review May 2, 2026 20:08
@jack-arturo

Copy link
Copy Markdown
Member

Thanks for the project CRUD work here. I created a maintainer refresh branch/PR at #60 with attribution preserved in the cherry-picked commits.

What changed in the refresh:

  • stacked on feat(toggl): add time entry task tools #58 so project CRUD reuses the shared successful-empty-body/write behavior instead of carrying the duplicate fix here
  • changed project writes to use the shared single-attempt write path
  • added server.json, cache, API, and MCP handler coverage for the public tools
  • preserved the direct project lookup optimization while keeping non-404 Toggl errors visible during fallback workspace probing

I’m leaving this PR open for traceability while #60 moves through review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants