Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
OPENPROJECT_URLNoInstance root URL, e.g. https://openproject.example.com. A trailing /api/v3 is tolerated and stripped.
OPENPROJECT_API_KEYNoAPI key from My account → Access tokens. Sent as HTTP Basic apikey:<token>.
OPENPROJECT_MCP_OTELNoReserved for OpenTelemetry tracing. Accepted but not yet wired to anything in this release.false
OPENPROJECT_MCP_DISABLENoComma-separated group tags to remove whole tool groups at startup.
OPENPROJECT_OAUTH_TOKENNoOAuth bearer token, as an alternative to the API key.
OPENPROJECT_MCP_INSECURENoAllow --transport http to start without auth tokens. Local development only.false
OPENPROJECT_MCP_CACHE_TTLNoTTL in seconds for the metadata cache (statuses, types, priorities, schemas).300
OPENPROJECT_MCP_CA_BUNDLENoPath to a CA bundle (PEM) for instances behind a private CA. TLS is always verified.system trust store
OPENPROJECT_MCP_HTTP_HOSTNoBind address for --transport http.127.0.0.1
OPENPROJECT_MCP_HTTP_PORTNoPort for --transport http.8000
OPENPROJECT_MCP_LOG_LEVELNoDEBUG, INFO, WARNING, ERROR or CRITICAL (case-insensitive).INFO
OPENPROJECT_MCP_READ_ONLYNoServe read tools only: every write, destructive and admin tool is removed at startup.false
OPENPROJECT_MCP_LOG_BODIESNoLog request/response bodies — only at DEBUG level, with credentials redacted. Development use only.false
OPENPROJECT_MCP_LOG_FORMATNotext or json. Logs always go to stderr.text
OPENPROJECT_MCP_ADMIN_TOOLSNoExpose the three admin-gated membership write tools (hidden by default).false
OPENPROJECT_MCP_AUTH_TOKENSNoComma-separated bearer tokens accepted by --transport http. Every request must carry Authorization: Bearer <token>.
OPENPROJECT_MCP_MAX_RETRIESNoRetry budget for idempotent requests.3
OPENPROJECT_MCP_DOWNLOAD_DIRNoDirectory where download_attachment writes files (created if missing; default is relative to the server's working directory)../openproject-downloads
OPENPROJECT_MCP_POOL_TIMEOUTNoSeconds to wait for a free connection from the pool.5
OPENPROJECT_MCP_READ_TIMEOUTNoSeconds to wait for response data.30
OPENPROJECT_MCP_WRITE_TIMEOUTNoSeconds to wait while sending request data (uploads).60
OPENPROJECT_MCP_ACCEPT_LANGUAGENoSent as the Accept-Language header; OpenProject localizes validation messages accordingly.
OPENPROJECT_MCP_CONNECT_TIMEOUTNoSeconds to wait for a TCP/TLS connection to OpenProject.10
OPENPROJECT_MCP_MAX_CONNECTIONSNoConnection pool size toward OpenProject.10
OPENPROJECT_MCP_MAX_DOWNLOAD_MBNoSize cap for attachment downloads, in MiB.100

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": true
}
logging
{}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
extensions
{
  "io.modelcontextprotocol/ui": {}
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
search_work_packagesA

Find work packages by text when you do not know their ids.

Use this first whenever a user names a ticket instead of numbering it, then feed the returned id into get_work_package, update_work_package or list_work_packages.

Returns the standard list envelope: compact rows (id, subject, type, status, priority, assignee, project, dates, progress) plus pagination with total/page/page_size/has_more.

Pitfalls: search filters, it does not rank, so a broad query returns a lot — narrow it with project_id, or switch to list_work_packages when you want structured filters (assignee, due date, version) rather than text. Attachment-content matching in 'fulltext' mode depends on instance database configuration and is reported honestly in notes.

For structured filtering use list_work_packages; for one work package's full detail (description, custom fields, children) use get_work_package.

list_work_packagesA

List work packages with structured filters — the workhorse read tool.

Handles "assigned to me", "overdue" via parameters, not separate tools: overdue → due_before=<today>; unassigned → assignee=['none']; nearly done → percentage_done_min=80; subtasks of a ticket → parent_id=<id>.

Returns the standard list envelope: rows plus pagination, plus groups when group_by is set and sums when show_sums is set — both computed server-side over the whole filtered set, independent of paging; never re-add them from one page's rows.

Pitfalls: returns open work packages only unless status_scope or status_ids is given (status_ids overrides status_scope), so say so when reporting counts. Status, type, priority and version ids come from get_project_metadata; user ids (assignee, author, responsible, watcher) from search_principals — never guess either. Date filters take YYYY-MM-DD, both bounds inclusive.

For text lookups use search_work_packages; for one work package's description, custom fields and children use get_work_package.

get_work_packageA

Read one work package in full: description, dates, custom fields, parent and progress.

This is the tool to call once a search or list has given you an id, and the only way to read a work package's description text. The lock_version in the result is what update_work_package needs for a safe concurrent edit.

Returns every core field, custom_fields in the canonical [{key, name, type, value, value_ids}] shape (only fields that have a value), an available map saying whether this work package exposes dev links, meetings or file links, and any requested includes.

Pitfalls: includes are capped at 20 — a truncated children list means you should call list_work_packages(parent_id=…) for the rest, which more_via spells out verbatim. A sub-resource that 403s or 404s (module off, no permission) degrades into a notes entry instead of failing the whole read.

For the comment thread use list_work_package_comments; for attachment bytes use download_attachment; for linked PRs and commits use get_work_package_git_activity.

create_work_packageA

Create a work package, validated through OpenProject's own form endpoint first.

Use it for new tasks, bugs, subtasks (parent_id) and milestones (date). The form pre-flight surfaces an invalid status, a missing required custom field or a disallowed type as structured violations with the allowed values, before anything is written.

Returns the created work package in full detail, including its new id, lock_version and resolved custom fields.

Pitfalls: type, status and priority take names or ids; versions, assignees and parents need numeric ids. Custom fields must exist on the schema — check get_work_package_schema when unsure.

To change it afterwards use update_work_package; to attach a file to an existing work package use upload_attachment.

update_work_packageA

Change any writable field of a work package, with optimistic locking done properly.

Use it to assign or unassign, move a status forward, re-schedule, re-parent, set progress, or write custom fields — one tool instead of many.

Returns the updated work package in full detail, including the new lock_version to use for a follow-up edit.

Pitfalls: omitted parameters are left alone, while passing null clears a field (assignee, responsible, version, sprint, parent, dates, description). A 409 error means somebody else changed the work package first — the error carries the fresh lock_version and the conflicting fields, so re-read and retry deliberately.

Ids come from get_work_package / list_work_packages; status, priority, type and version values come from get_project_metadata; sprint ids come from list_sprints.

delete_work_packageA

Permanently delete a work package and everything attached to it.

Use only on explicit user instruction. Deletion removes the work package with its comments, attachments, time entries and relations, and OpenProject offers no API-side undo.

Returns a small confirmation object once OpenProject accepts the deletion.

Pitfalls: children are not deleted with the parent, so check get_work_package(id, include=['children']) first and decide what happens to them. If you only want the work package out of the way, update_work_package(id, status=<a closed status>) is almost always the better answer — get_project_metadata lists which statuses this instance treats as closed.

bulk_update_work_packagesA

Preview or apply up to 50 work-package updates, each with its own changes.

First call with dry_run=true (default). Review each before/after diff and validation error, then submit the same updates with dry_run=false and each preview's lock_version. Omitted fields remain untouched; null clears clearable fields; target_versions=[] clears version assignments. Status/type/priority accept names or ids, as in update_work_package.

All items are validated before any update. If any preflight fails, nothing is applied; correct those items or submit a valid subset. Execution is sequential and NOT atomic: concurrent edits can still conflict after validation. Each item reports its result; partial_failure is true when some apply and others fail. Item notes flag values OpenProject will ignore or ignored, such as story_points without Backlogs. An unknown outcome means a connection/server failure may have happened after committing: re-read that item before retrying. Writes are never automatically retried or rolled back. Never rerun successful items. Notifications use the same notify flag for the whole batch.

list_work_package_commentsA

Read the comment thread and change history of a work package.

Use this whenever the question is "what did people say about this ticket" or "what changed on it": it returns the full activity journal — comment entries (author, markdown text, internal flag, timestamps) and field-change entries whose details are parsed into {field, from, to} (for example {"field": "Status", "from": "New", "to": "In progress"}).

Returns the standard list envelope: items plus pagination{total,page,page_size,has_more} and notes.

Pitfalls. OpenProject's activities endpoint is unpaginated — this tool fetches the entire journal on every call and pages it here, so page/page_size cost the same upstream but keep the reply small. Entries are ordered oldest first, so ask for the last page to see the latest discussion. Comment text is cut at max_comment_chars and marked truncated: true; pass that entry's id back as activity_id to read it in full.

Cross-references: post a comment with add_work_package_comment; the work package itself (description, custom fields, watchers) comes from get_work_package; files referenced in a comment are listed by list_attachments.

add_work_package_commentA

Post a comment on a work package.

Use this to reply in a ticket's thread, record a decision, or leave a handover note. Returns the created journal entry (activity id, author, markdown text, internal flag, timestamps) — the same shape list_work_package_comments returns, so the id can be reused.

Pitfalls. Every call creates a new comment; it is not idempotent, so do not retry blindly after a timeout — read the thread first. internal=true is refused on OpenProject below 16.0 because those versions accept the flag and publish the comment anyway; upgrade or post publicly, deliberately. notify=false suppresses notifications only, the comment is still visible to everyone who can see the work package.

Cross-references: read the thread with list_work_package_comments; change fields (status, assignee, dates) with update_work_package rather than describing the change in prose; attach a file with upload_attachment.

edit_work_package_commentA

Rewrite the text of an existing work-package comment.

Use this to fix a typo, correct a wrong statement, or extend a note you just posted. Returns the updated journal entry (activity id, author, markdown text, internal flag, timestamps) in the same shape list_work_package_comments returns.

Pitfalls. Only comment entries are editable: the journal also holds field-change entries ("Status changed from New to In progress"), which OpenProject records automatically and refuses to alter — this tool rejects those locally, before any write. Editing needs the edit-work-package-comments permission (or edit-own for your own comments); a 403 means the account may read the thread but not rewrite it. The edit replaces the text entirely and OpenProject keeps no API-visible history of the previous version, so do not use it to "undo" — post a correcting comment when the record matters. Editing does not notify anyone.

Cross-references: read the thread and get activity ids with list_work_package_comments; post a new comment with add_work_package_comment; change fields (status, assignee, dates) with update_work_package instead of describing them in prose.

add_work_package_watcherA

Subscribe a user to a work package's notifications.

Use this when someone should be kept in the loop on a ticket without being assigned to it. Watchers receive OpenProject's notifications for comments and changes. Returns the watcher (user id and name), the watch state after the call, and whether this call actually changed anything.

Pitfalls. The user must already be able to see the work package; OpenProject answers 422 with a violation on user otherwise, and adding someone other than yourself needs the add-work-package-watchers permission (adding yourself only needs view access). Calling twice is harmless: the second call reports changed: false. Watching is not assignment — use update_work_package(assignee=...) for that.

Cross-references: get_work_package(include=['watchers']) lists who already watches; remove_work_package_watcher is the reverse; list_work_package_comments shows what watchers are being notified about.

remove_work_package_watcherA

Unsubscribe a user from a work package's notifications.

Use this to stop notifying someone who no longer needs the updates. Returns the user, the watch state after the call (always not watching) and a confirmation message.

Pitfalls. OpenProject answers the same 204 whether or not the user was watching, so changed comes back null — do not report "removed" as proof that they were subscribed. Removing another user needs the delete-work-package-watchers permission; removing yourself only needs view access. A 404 means the user id is unknown (or the work package is), not that they were not watching. This does not unassign anyone and does not remove them from the project.

Cross-references: get_work_package(include=['watchers']) shows who watches today; add_work_package_watcher is the reverse.

create_work_package_relationA

Link two work packages (blocks, follows, duplicates, relates, ...).

Use this to record a dependency the schedule or the reader needs to know about: "ship the client layer follows design sign-off", "this duplicates #4321". Returns the created relation with its id, type, reverse_type, both work packages, lag and description.

Pitfalls. OpenProject stores one canonical direction per pair, so the passive spellings are rewritten on save: creating precedes from A to B comes back as B follows A, with from_work_package and to_work_package swapped. That is the same fact, not an error — read type and reverse_type from the result rather than assuming what you sent. Only one relation may exist between two work packages: a second one answers 409 conflict, and changing it means update_work_package_relation on the existing id. A relation that would close a scheduling cycle is rejected with a validation error. Creating a follows relation can move dates, since OpenProject reschedules the successor.

Cross-references: get_work_package(include=['relations']) lists what a work package is already linked to and produces relation ids; update_work_package_relation edits one; delete_work_package_relation removes it; parent/child hierarchy goes through update_work_package(parent_id=...).

update_work_package_relationA

Change an existing relation's type, lag or description.

Use it to widen the gap between a predecessor and its successor, to correct a link that was created with the wrong type, or to explain why two work packages are connected. Returns the updated relation.

Pitfalls. At least one of type, lag or description must be given. Relations carry no lock version, so this is a plain overwrite with no conflict detection — a concurrent edit is silently replaced; re-read the relation if that matters. The two work packages cannot be changed here: delete the relation and create a new one instead. Raising the lag on a follows relation reschedules the successor, so dates can move.

Cross-references: create_work_package_relation makes one; delete_work_package_relation removes it; get_work_package(include=['relations']) lists the ids.

delete_work_package_relationA

Remove the link between two work packages.

Use it when a dependency no longer holds. Neither work package is touched, only the relation between them. Returns a small confirmation object.

Pitfalls. Deleting a follows relation drops the scheduling constraint, so OpenProject may reschedule the work package that was waiting. There is no undo; recreating the relation with create_work_package_relation is the only way back. A second delete of the same id answers 404. Parent/child hierarchy is not a relation — clear it with update_work_package(parent_id=null).

Cross-references: get_work_package(include=['relations']) shows what would be removed; update_work_package_relation changes a relation instead of removing it.

toggle_comment_reactionA

React to a work-package comment with an emoji, or take your reaction back.

Use this for the lightweight acknowledgement a comment does not deserve: 👍 on a decision, 👀 to say you are looking at it, 🎉 when something shipped. Returns the comment's full reaction state afterwards — every emoji on it with the people who picked it — plus reacted, which says whether you are now among them.

Pitfalls. This is a toggle, not an add: calling it twice with the same reaction leaves the comment exactly as it started, so it is not safe to retry blindly after a timeout — read reacted from the result instead. Reactions belong to the authenticated account; you cannot react on someone else's behalf and cannot remove their reaction. Only comment entries can be reacted to — a field-change journal entry ("Status changed from New to In progress") is refused by OpenProject with a 400. The feature needs OpenProject 16.0 or newer; an instance known to be older is refused up front with a version hint rather than pretending to have reacted, and one whose version is not readable answers 404 instead.

Cross-references: list_work_package_comments reads the thread and produces activity ids; add_work_package_comment says something in words when an emoji is not enough; get_instance_info reports the detected OpenProject version.

set_work_package_reminderA

Set, change or clear your personal reminder on a work package.

Use this when something should resurface later: "remind me about this on Monday", "ping me an hour before the release". Reminders are private — only you see yours, and only you are notified. The tool upserts: it looks for your active reminder on the work package and creates one if there is none, updates it if there is. Returns action (created/updated/deleted/unchanged) and the resulting reminder.

Passing remind_at=null deletes the reminder. That is the documented way to clear it rather than a destructive operation — nothing but your own pending notification is removed, the work package and its history are untouched — so this tool does not ask for confirm. Set a new time to get it back.

Pitfalls. OpenProject allows exactly one active reminder per work package per person, so a second "create" becomes an update of the first — there is no way to stack two. remind_at must carry a timezone; a bare '2026-08-03T09:00' is refused rather than guessed at. A reminder in the past is rejected by the instance. Once a reminder has fired it disappears from the API, so a later call creates a fresh one rather than reviving it. Reminders are personal: you cannot set one for a colleague — add them as a watcher or mention them in a comment instead.

Cross-references: list_reminders shows everything you have pending; add_work_package_watcher notifies someone about changes rather than at a chosen time; add_work_package_comment with an @-mention is how you get another person's attention.

list_remindersA

List your own upcoming work-package reminders.

Use this to answer "what have I asked to be reminded about", to check whether a reminder is already set before creating another one, or to find the work packages you deferred. Returns the standard list envelope: items of {id, remind_at, note, work_package, creator} plus pagination and notes.

Pitfalls. Reminders are personal — this only ever shows the ones the authenticated account created, never a colleague's, and there is no way to list someone else's. It only shows reminders that are still upcoming: once one has fired (or was completed) OpenProject drops it from this collection, so an empty result does not mean nothing was ever scheduled. The work package each reminder points at is in work_package; a reminder is not a work package and its id is not one.

Cross-references: set_work_package_reminder creates, moves or deletes one; list_notifications shows what OpenProject has actually notified you about, including fired reminders; get_work_package opens the ticket a reminder points at.

execute_custom_actionA

Run an instance-defined one-click action on a work package.

Custom actions are shortcuts an administrator configured — "Accept and assign to me", "Reject", "Move to review" — that apply several field changes at once, sometimes under conditions (role, status, project). Use one when get_work_package(include=['custom_actions']) offers it, instead of reproducing its effects field by field. Returns the updated work package row (subject, type, status, priority, assignee, project, dates, percentage_done, updated_at).

Pitfalls. The action decides what changes; this tool cannot influence it, and OpenProject does not report which fields it touched — compare the returned row with what you read before, or call get_work_package again for the full detail (including the new lock_version for your next update). Availability is per work package: an action listed on one ticket may 403 on another because its conditions no longer hold, and a 422 usually means the resulting work package would be invalid (a required field the action leaves empty). Writes are never retried automatically — a conflict comes back with the fresh lock_version so you can re-read and decide.

Cross-references: get_work_package(include=['custom_actions']) produces the ids and says which are available right now; update_work_package is the explicit alternative when you know exactly which fields to set; list_work_package_comments shows what the action recorded in the journal.

list_attachmentsA

List the files attached to one container.

Containers are work packages, wiki pages, meetings, documents, budgets and comments. Use this to discover attachment ids before calling download_attachment, or to check what a work package already carries. The upstream collection is not paginated, so it is fetched in full: the envelope always reports has_more=false and a total equal to the row count.

Returns the standard list envelope; each row has id, file_name, size_bytes, content_type, description, author, created_at and status. status is the virus-scan state — 'uploaded' and 'scanned' are downloadable, 'quarantined' files are not, and anything else is still being scanned and is readable only by its uploader.

Pitfalls: container_id identifies the container, not the file. A 404 means the container does not exist or the module providing it (meetings, budgets, documents) is not enabled on this instance. Forum posts are a valid API container but have no discovery path here.

Related: download_attachment fetches the bytes for one row, upload_attachment adds a file to the same containers, and get_work_package(include=['attachments']) returns these rows inline for a single work package.

download_attachmentA

Download an attachment's bytes to a file on the machine running this server.

Use it once list_attachments (or get_work_package(include=['attachments'])) has given you an attachment_id. Metadata is read first, then the bytes are streamed to disk in chunks with progress notifications, so a large file neither stalls the call nor buffers in memory.

Returns path, file_name, size_bytes, content_type and the SHA-256 of the bytes (use it to verify or de-duplicate). With return_image=true an image of at most 1 MB comes back as an inline image block as well.

Pitfalls: the file is written on the server's machine, which is the user's machine only in a local (stdio) deployment — tell the user the returned path rather than assuming they can see it. Quarantined attachments fail with attachment_quarantined and are never fetched. An attachment whose virus scan is unfinished answers 401 for everyone except its uploader. Transfers above OPENPROJECT_MCP_MAX_DOWNLOAD_MB (default 100) are refused up front and aborted mid-stream, leaving no partial file. A name collision in the target directory saves as 'name (2).ext' and says so in notes.

Related: list_attachments produces attachment_id; upload_attachment is the reverse direction.

upload_attachmentA

Attach a local file to a work package, wiki page, meeting, document, budget or comment.

Use it when a file that already exists on the server's machine should be added to an existing container. The file's existence and its size against this instance's maximumAttachmentFileSize are checked locally first, so an oversized file fails instantly instead of after the transfer.

Returns the created attachment row (id, file_name, size_bytes, content_type, description, author, created_at, status) — the id feeds download_attachment.

Pitfalls: uploading to a container needs edit permission on that container, so to give a brand-new work package its files use create_work_package(attachment_paths=[...]) instead, which uploads the files unattached and claims them on create. Instances may restrict extensions; a rejected type comes back as validation_failed with the allowlist hint and nothing is stored. The stored name comes from file_name (or the path's basename), never from the multipart part.

Related: list_attachments shows what a container already holds; download_attachment is the reverse direction.

delete_attachmentA

Permanently delete one attached file from OpenProject.

Use it only on explicit user instruction, for example to remove a file uploaded to the wrong work package or a superseded document. The attachment's metadata is read first so the result names the file and the container it was attached to, and so an unknown id fails before anything is removed.

Returns the attachment id, the file name, its container and a confirmation message.

Pitfalls: this deletes the file itself, not a link to it — every work package, wiki page or comment that embedded it loses the image or download. Deleting needs edit permission on the container (or authorship for a file that has no container yet), so a 403 can follow a successful read. A 404 means the id is unknown or already deleted; a second call on the same id answers 404 rather than succeeding. Removing a file does not remove the comment or work package that referenced it.

Related: list_attachments shows the ids and file names of everything a container holds; upload_attachment adds a replacement; download_attachment saves a copy first if the bytes are still wanted.

list_file_linksA

List the external-storage files (Nextcloud, OneDrive/SharePoint) linked to a work package.

File links are OpenProject's other kind of file: instead of living inside OpenProject like an attachment, the document stays in a connected storage and the work package points at it. Use this to answer "which documents belong to this ticket" — and pair it with list_attachments, because the two lists are disjoint and neither implies the other.

Returns the standard list envelope, fetched in full (has_more is always false). Each row carries file_name, the storage it lives on, the file's origin_id inside that storage, mime_type, the creator and — the useful part — open_url and download_url. Those are absolute OpenProject URLs that redirect to the storage once OpenProject has resolved the link, so hand them to the user: they need the user's own OpenProject login, this server cannot fetch the bytes, and download_attachment does not work on them.

Pitfalls: this needs the storages module and a storage connected to the project. When it is missing (404) or this account may not read the links (403) the call still succeeds with an EMPTY list and a note explaining which — read notes before saying a ticket has no documents. An empty list is never proof either: an account lacking the 'view file links' permission gets an empty 200 rather than a 403, which is exactly what that note says. permission carries the storage's own wording — 'View allowed' means the URLs will work, 'View not allowed', 'Not found' and 'Error' mean they will not, and null means the storage said nothing. Creating and deleting file links, and browsing the remote storage, are out of scope for this server — do them in the OpenProject UI.

Related: list_attachments covers files stored inside OpenProject, download_attachment fetches those bytes, and get_work_package gives the ticket the links belong to.

list_projectsA

List projects, filtered server-side, one page at a time.

Use this to turn a project name into the id or identifier that every other tool consumes, to enumerate the sub-projects of a parent, or to review which projects are off track. It is the id-producing path for every project_id parameter in this server.

Returns the standard list envelope: items of {id, identifier, name, active, public, parent, status_code, workspace_type} plus pagination with total/page/page_size/has_more. Nothing is truncated silently — page explicitly until has_more is false.

Pitfalls: search matches name and identifier only (not descriptions); parent_id returns direct children, so a deep hierarchy needs one call per level; status_code is a code such as on_track, never a translated label. On OpenProject 17.x this listing deliberately mixes plain projects with programs and portfolios — workspace_type says which each row is. in_phase tests phase dates ("which projects are in Executing today"), so projects whose phases carry no dates never match it.

For a single project's description and status explanation use get_project. For the types, versions, categories and time-entry activities valid inside a project use get_project_metadata. To list a project's work packages use list_work_packages(project=...).

get_projectA

Read one project in full.

Use it after list_projects when you need the description, the status explanation or the parent of a specific project — or to verify that an id or identifier a user gave you actually resolves.

Returns {id, identifier, name, active, public, parent, status_code, workspace_type, description, status_explanation, created_at, updated_at}. Rich text comes back as markdown raw; html is dropped.

Pitfalls: status_code is one of on_track, at_risk, off_track, not_started, finished, discontinued — an empty status_code means the project has no status set, not "on track". A 404 here means the id/identifier is wrong or the project is archived and invisible to this user; the error hint says which spelling to try next.

For the ids valid inside this project (types, versions, categories, time-entry activities) call get_project_metadata(project_id=...); for its work packages call list_work_packages(project=...).

create_projectA

Create a project, validated through OpenProject's own form endpoint first.

Use it for a new workspace or, with parent_id, for a subproject of an existing one. The call runs POST /projects/form before committing, so a taken identifier, an unusable parent or a bad status comes back as violations naming the attribute instead of an opaque failure — and the identifier OpenProject derives from the name is used verbatim on the commit.

Returns the created project: {id, identifier, name, active, public, parent, status_code, description, status_explanation, created_at, updated_at}. Keep the id — every other tool's project_id accepts it, as does the identifier.

Pitfalls: creating projects usually requires the 'create project' permission or admin rights, so a 403 here is about the account, not the payload. The new project starts with the instance's default modules and types enabled — check get_project_metadata(project_id=...) before creating work packages in it. Members are not copied from the parent; add them with create_membership (admin-gated: hidden unless the server sets OPENPROJECT_MCP_ADMIN_TOOLS=1).

Cross-references: list_projects finds the parent id; update_project changes any of these fields afterwards; get_project_metadata lists the types, versions and categories valid inside the result.

update_projectA

Change a project's name, description, visibility, parent, status or archived state.

Use it to record a status change with its explanation ("at_risk because the vendor slipped"), to rename or re-parent a project, to publish it, or to archive it with active=false. The change is validated through POST /projects/{id}/form first, so rejected values come back as violations naming the attribute.

Only the parameters you pass are sent — omitted fields are never rewritten, so two agents editing different fields do not clobber each other. Projects carry no lockVersion upstream, so there is no version to echo and no lock parameter here.

Returns the updated project in the same shape as get_project.

Pitfalls: description and status_explanation REPLACE the stored text rather than appending to it. active=false archives, which is not deletion but does hide the project and freeze its work packages. Changing identifier is deliberately not offered — it breaks every existing link to the project.

Cross-references: get_project to read the current values first; delete_project to remove a project for good; list_projects(active=false) to find archived ones.

delete_projectA

Schedule the permanent deletion of a project and everything inside it.

Use only on an explicit, specific instruction. Deletion CASCADES: every subproject, work package, comment, attachment, time entry, version, wiki page and membership of this project goes with it, and OpenProject offers no API-side undo. If the goal is only to get the project out of the way, update_project(active=false) archives it instead — reversible, and it preserves the data.

Deletion is ASYNCHRONOUS upstream: OpenProject accepts the request and runs it as a background job. This tool therefore returns {scheduled: true, job_id, message}, never a claim that the project is already gone — for a large project the data disappears over minutes and get_project may still answer during that window.

Pitfalls: deleting normally requires admin rights (403 otherwise). A 404 means the id or identifier is wrong, or the project was already deleted. Because the work runs in the background, a later failure inside the job is not visible here; confirm with get_project (it should eventually 404) rather than assuming success.

Cross-references: get_project to check what you are about to destroy; list_projects(parent_id=...) to see the subprojects that would go with it; update_project(active=false) for the reversible alternative.

copy_projectA

Copy a project — its settings, and optionally its work packages — into a new one.

Use it to spin a new engagement or release off a template project, which is the only way to reproduce a project's members, versions, categories and enabled modules in one call. The request goes through POST /projects/{id}/copy/form first, so a name that derives a taken identifier comes back as violations naming the attribute instead of a failed background job.

Copying is ASYNCHRONOUS: OpenProject queues a job and answers immediately. This tool therefore returns {scheduled: true, job_id, status, message, notes} and NEVER claims the copy exists — a large project takes minutes. Poll get_job_status(job_id=...) until status is 'success' (it then reports the new project) or 'failure'.

Pitfalls: only include_work_packages and notify are exposed; every other copy flag (members, versions, wiki, boards, file links) keeps this instance's own default, which the form fills in — so the copy can contain more than the two parameters suggest. A 403 means the account may not copy this project (it needs the 'copy project' permission on the template plus the right to create projects). Work packages come across with their relations, but time entries and comment histories do not.

Cross-references: get_job_status follows the job to completion; list_projects or get_project confirms the result; create_project makes an empty project instead; update_project renames the copy afterwards.

get_job_statusA

Check whether a background job (a project copy, a scheduled deletion) has finished.

OpenProject runs copies, deletions and exports asynchronously and hands back a job id. This is the only way to learn what happened to one: call it after copy_project or delete_project and wait for a terminal state before reporting an outcome to the user.

Returns {id, status, finished, successful, message, project, result_url, notes}. status is 'in_queue' or 'in_process' while the job runs and 'success', 'failure', 'error' or 'cancelled' once it is over; finished and successful are derived from it, and successful stays null while the job runs rather than defaulting to false. A finished copy reports the new project in project and the URL it lives at in result_url.

Pitfalls: a 200 does not mean the job worked — read status. Polling is on you: wait a few seconds between calls rather than looping tightly. OpenProject drops job statuses after a while, so a 404 can mean 'long finished' as easily as 'wrong id'; confirm with get_project or list_projects. When a job fails, message is what OpenProject recorded — there is no API to retry it, so the underlying tool has to be called again deliberately.

Cross-references: copy_project and delete_project produce the job_id; get_project / list_projects verify what the job actually did.

set_project_favoriteA

Add or remove a project from the authenticated user's favorites (OpenProject 17+).

Favorites are per user, not per project: this changes what the account behind OPENPROJECT_API_KEY sees starred on its own overview page, and nothing about the project itself or about anybody else's view. Use it when the user asks to pin, star or favorite a project they work in.

Returns {id, favorite, message} — favorite is the state now in effect. The call is idempotent: favoriting an already-favorited project succeeds again.

Pitfalls: this endpoint only exists from OpenProject 17. When the instance reports a version older than that the call is REFUSED before anything is sent, with the detected version in the message, because there is no downgrade that would achieve the same thing — favorite the project in the web UI instead. When it reports no version at all the request IS sent, since an unreported version says nothing about the endpoint. A 404 is ambiguous on purpose in the hint: it means either the project does not exist for this account or the endpoint is missing. This is not project 'status' and not a work-package watcher.

Cross-references: list_projects(favorites_only=true) lists the current favorites (and works on older instances too); get_project resolves an identifier first; get_instance_info reports the detected OpenProject version.

list_project_phase_definitionsA

List the instance-global phase definitions — the vocabulary of the project life cycle.

Use it to learn which phases (Initiating, Planning, …) and gates this instance defines, and to get the definition id or name that list_projects(in_phase=...) accepts.

Returns the standard list envelope of {id, name, start_gate, start_gate_name, finish_gate, finish_gate_name} rows. Gates are the checkpoints a phase can begin or end with; a definition without gates has both flags false.

Pitfalls: definitions are the instance-wide catalog, not any project's actual phases — a project may deactivate phases or set no dates. Phase dates are not exposed by the API at all. Requires OpenProject 16.1+ and the view_project_phases permission in at least one project; older instances 404 (the error hint says so).

Cross-references: list_projects(in_phase=...) to find the projects a phase covers today; get_project_phase for one project's phase record (its id comes from a work package's project_phase).

get_project_phaseA

Read one project's phase record: name, active flag and its definition.

Use it after get_work_package surfaced a project_phase reference and you need to know which project and definition that phase belongs to, or whether it is still active.

Returns {id, name, active, definition, project, created_at, updated_at}.

Pitfalls: the API has no phases index — ids only come from work packages' project_phase references. Phase dates are not exposed by the API (the notes say so), so "which projects are in this phase now" goes through list_projects(in_phase=...) instead. A 404 covers a wrong id, a phase invisible to this user (view_project_phases), and instances that predate project phases (16.1).

Cross-references: list_project_phase_definitions for the instance-wide catalog and gates; list_projects(in_phase=...) for date-based phase queries.

list_queriesA

List the saved work-package views (queries) this user can open.

Use it to discover what a team already tracks — "Sprint board", "My open bugs", "Overdue in Platform" — before hand-building filters: running someone's saved view with run_query reproduces exactly what they see in the UI, including their grouping and sums.

Returns the standard list envelope: rows of {id, name, project, public, starred, updated_at} plus pagination. project is null for a global query (saved outside any project); public false means the query is private to its owner, and only the owner's queries are visible to this account.

Pitfalls. Query ids are instance-wide, not per project — never guess one, take it from here. This lists definitions only; it never runs them, so nothing here says how many work packages a query returns.

Cross-references: run one with run_query(query_id=…); build an ad-hoc query instead with list_work_packages; project ids come from list_projects.

run_queryA

Run a saved view and get its work packages — the fastest way to answer with a team's own definition of "the sprint" or "our bugs".

OpenProject queries run on read: this returns the rows as they are right now, in the stored order and grouping. The result is the standard list envelope — items of compact work-package rows, pagination, plus groups when the query groups and sums when it asks for totals — with one addition: query carries the stored definition (name, project, readable filters, group_by, sort_by), so the rows can be interpreted without a second call.

Pitfalls. groups and sums are computed server-side across the entire result set, not the page in front of you — never re-add them from items. Omitting page_size keeps the query's own page size, which may be much larger than 20. override_filters replaces the stored filters instead of narrowing them, and never edits the saved query. A 422 means the filter set is invalid for this query's context (a project-scoped filter on a global query, an unknown custom field); violations names the attribute.

Cross-references: find query ids with list_queries; equivalent ad-hoc filtering lives in list_work_packages; open a single row with get_work_package.

save_queryA

Save a filter set as a reusable OpenProject view.

Use it to keep a filter combination — "Overdue in Platform", "My open bugs" — instead of rebuilding it each session: the view shows up in the OpenProject UI too, and run_query reproduces it exactly. Prove filters with list_work_packages first.

Validated via POST /queries/form first, so an invalid filter name, unsupported operator, or project-scoped filter on a global view comes back as violations naming the attribute — nothing is saved. Returns the stored definition: {id, name, project, public, starred, filters (as readable sentences), group_by, sort_by, display_sums, updated_at, notes}. Keep the id — run_query takes it.

Pitfalls. If OpenProject keeps fewer filters than were sent, notes says so — read filters rather than assuming a match. Custom-field filters (customField12) are sent as plain values, since a list-typed one can't be told apart from a text one without asking the instance; a failing one means nothing was saved — save that view in the UI instead. Editing and deleting saved views isn't offered here: use the OpenProject UI.

Cross-references: list_queries lists what exists (with ids); run_query(query_id=...) runs this view; list_work_packages validates filters first; list_projects supplies project_id.

list_notificationsA

Read the authenticated user's OpenProject inbox.

Use this to answer "what needs my attention?", "was I mentioned anywhere?" or "what changed on the things I watch?" — it is the only tool that sees notifications, and it always reports the inbox of the token owner, never another user's.

Returns the standard list envelope: items of {id, reason, read, created_at, actor, project, resource} plus pagination with total/page/page_size/has_more. resource is the thing the notification is about — {id, type, title}, usually type='WorkPackage', so resource.id feeds straight into get_work_package or list_work_package_comments.

Pitfalls. Several changes to the same work package are aggregated into one notification, so the count is not a count of events. Reading a notification here does not mark it read — that is mark_notifications. unread_only=false can return a very long history; keep a page size that fits your reply. A notification whose project the token owner has lost access to disappears from the inbox entirely.

Cross-references: mark specific rows read with mark_notifications; clear the whole (optionally filtered) inbox with mark_all_notifications_read; open the underlying ticket with get_work_package(resource.id) and its discussion with list_work_package_comments(resource.id).

mark_notificationsA

Mark specific notifications read (or unread) in one bulk request.

Use it after you have actually handled what a notification was about, so the user's inbox reflects reality. It is the id-consuming counterpart of list_notifications: pass the row id values from that tool.

Returns {marked, read, ids, message}. OpenProject answers the bulk endpoint with 204 No Content, so marked is the number of ids the call covered — ids that were already in the requested state, or that belong to someone else's inbox, are simply not changed.

Pitfalls. Marking is idempotent, so a retry after a timeout is safe. Pass the notification id, not resource.id — the work package id underneath is a different number entirely. Unknown ids do not fail the call, so do not treat success as proof that every id existed.

Cross-references: get the ids from list_notifications; to clear an entire (optionally filtered) inbox in one go use mark_all_notifications_read.

mark_all_notifications_readA

Mark everything matching the filters as read — the whole inbox by default.

Use it for "clear my notifications" or "I have dealt with everything in project X". Called with no arguments it marks every unread notification of the token owner as read, across all projects; reason and project_id narrow that blast radius, they do not create a preview. Check what is about to disappear with list_notifications(unread_only=true, ...) using the same filters first — there is no undo beyond re-marking individual ids unread.

Returns {marked, read, message}, where marked is how many unread notifications matched the filters at the moment of the call (counted immediately before the bulk update, so a notification arriving during the call may be counted differently than it was marked).

This tool only ever marks read. There is deliberately no "mark everything unread" twin: that is a mistake with no upside, and the reverse direction stays available per-id through mark_notifications(ids=[...], read=false).

Cross-references: preview or page the inbox with list_notifications; mark a handful of rows with mark_notifications.

list_time_entriesA

List logged time, filtered server-side, with an optional accurate total.

Answers "how much time went into this ticket?" or "how much did the team spend on project X in June?". Filters combine with AND, so project_id + user='me' + a date range is one call.

Returns the standard list envelope: items of {id, hours, spent_on, comment, user, activity, work_package, project} plus pagination. hours is a float (1.5 = 1h30), never an ISO duration. With sum_hours=true, the envelope also carries sums.total_hours over all matches and a groups bucket per activity with its own count/sums.total_hours — both over the whole filtered set, so never add pages up yourself.

Pitfalls. Without view-all-time-entries you see only your own entries, so a small total may mean "not allowed to see" rather than "nobody booked time". work_package_id scopes to that one work package — child work packages are not included; a parent's roll-up needs a query per child. Summing stops at 2000 entries, noted in notes; narrow the range or project rather than trusting the number.

Cross-references: book time with log_time; correct with update_time_entry, remove with delete_time_entry; activity ids/names from get_project_metadata; the work package from get_work_package.

log_timeA

Book time against a work package or a project.

Use it when the user says "log 2 hours on #1234" or "book half a day to project X". The call is validated through OpenProject's own form endpoint first, so an activity this project does not allow, a missing permission or a closed cost-reporting period comes back as a typed error listing what would be accepted — nothing half-written is left behind.

Returns the created entry: {id, hours, spent_on, comment, user, activity, work_package, project, created_at, updated_at, lock_version}. hours comes back as a float.

Pitfalls. This is not idempotent — calling it twice books the time twice, so never blind-retry after a timeout; list the day's entries first. The time is always booked for the token owner; you cannot log time on someone else's behalf through this tool. Logging time does not change the work package's status, estimate or progress — those are separate fields, and on instances that derive progress from work the percentage is read-only anyway. The work package's spent_hours reflects the new entry on the next read.

Cross-references: the activities and ids this project accepts come from get_project_metadata(project_id=...); review what is already booked with list_time_entries; fix a mistake with update_time_entry or delete_time_entry.

update_time_entryA

Correct an existing time entry.

Use it for the everyday fixes: wrong duration, wrong day, wrong activity, a comment that needs to say what actually happened. Only the parameters you pass are written; everything else is left exactly as it is.

Returns the updated entry in the same shape log_time returns.

Pitfalls. What can be moved between entries is limited: the work package and the project a time entry belongs to are not editable here — delete the entry and log it again where it belongs. Some instances report a lock_version for time entries and some do not; this tool reads the entry first and only echoes a lock version when one exists, so a concurrent edit surfaces as a conflict error with the fresh state rather than silently overwriting a colleague's correction. Editing time inside a closed cost-reporting period is refused by OpenProject with a validation error.

Cross-references: find the id with list_time_entries; remove the entry entirely with delete_time_entry; the valid activity names come from get_project_metadata.

delete_time_entryA

Permanently delete a logged time entry.

Use only on explicit user instruction, and only for genuinely wrong entries. Deletion removes the booked hours from every cost report and from the work package's aggregated spent time, with no API-side undo.

Returns a small confirmation object once OpenProject accepts the deletion.

Pitfalls. If the entry is merely on the wrong day, has the wrong duration or the wrong activity, update_time_entry is the better answer — it keeps the audit trail. Deleting someone else's entry needs an administrative permission and otherwise fails with permission_denied. Entries inside a closed cost-reporting period cannot be deleted.

Cross-references: find the id with list_time_entries; correct instead of deleting with update_time_entry.

list_versionsA

List versions (releases, milestones, sprints) you can assign work packages to.

Use it to turn "Sprint 12" or "release 2.1" into the version id that create_work_package/update_work_package need, to see which versions are still open, or to review a release plan's dates. With project_id it answers "what can I target in THIS project", which includes versions shared down from parent projects.

Returns the standard list envelope: items of {id, name, project, status, start_date, end_date, description, sharing, source} plus pagination and notes. A project-scoped listing is fetched in full, so has_more is false.

Pitfalls: project is the project that DEFINES the version, which for a shared version is not the project you asked about — assigning still works. status is open/locked/closed and OpenProject refuses to put work packages into a closed version. The instance-wide listing is capped at one page of 100; if more exist, pagination.has_more is true and notes says so — narrow with project_id rather than assuming you saw everything. include_sprints depends on the backlogs module: where it is not installed the versions still come back and notes explains the absence, so read notes before telling a user a project has no sprints.

Cross-references: create_version adds one, update_version moves its dates or closes it, delete_version removes it; get_project_metadata(project_id=...) returns the same versions alongside types and categories; to see what is IN a version use list_work_packages with a version filter. For standalone sprints (17.3+) use list_sprints/get_sprint.

create_versionA

Create a version (release, milestone or sprint) inside a project.

Use it to open a new sprint or plan a release before assigning work packages to it. The call goes through POST /versions/form first, so a duplicate name or an impossible date range comes back as violations naming the attribute instead of an opaque rejection.

Returns the created version {id, name, project, status, start_date, end_date, description, sharing, source, created_at, updated_at}. The id is what update_work_package(version=...) and update_version consume.

Pitfalls: end_date is the version's finish date and is written to the API's endDate field — passing a date here always lands (an older client dropped it silently). Creating versions needs the 'manage versions' permission in the project, so a 403 is about the account, not the payload. Versions are per project: sharing is the only way another project sees this one.

Cross-references: list_versions for what already exists (and for the ids); update_version to change dates or close it later; list_projects for the project id.

update_versionA

Change a version's name, dates, description, status or sharing.

Use it to move a sprint's dates, to close a finished release (status='closed'), or to widen sharing so a subproject can use the version. The change is validated through POST /versions/{id}/form first, so rejected values come back as violations naming the attribute.

Only the parameters you pass are sent, so concurrent edits to other fields survive. Versions carry no lockVersion upstream, so there is nothing to echo and no lock parameter here — a 409 would mean the resource itself changed, not a stale version.

Returns the updated version in the same shape as create_version.

Pitfalls: end_date writes the API's endDate — it lands, unlike in the old server. description REPLACES the stored text. Closing a version does not move or unassign its work packages; they keep pointing at it. The defining project cannot be changed — create a new version instead.

Cross-references: list_versions for ids and current values; delete_version when the version must really disappear; update_work_package(version=...) to move individual work packages between versions.

delete_versionA

Permanently delete a version.

Use it only for a version created by mistake. For a finished release or sprint, update_version(status='closed') is almost always the right answer: it keeps the history and stops new assignments.

Returns a small confirmation once OpenProject accepts the deletion. Work packages are NOT deleted — they simply lose their version — but that only happens on instances that allow the deletion at all.

Pitfalls: OpenProject refuses (422) to delete a version that work packages still reference; the error hint explains how to find them. Deleting a shared version affects every project that used it. The 'manage versions' permission is required, so a 403 is about the account.

Cross-references: list_versions for the id; update_version(status='closed') for the reversible alternative; list_work_packages to find what still points at the version before removing it.

list_sprintsA

List sprints (Scrum timeboxes) you can plan work packages into.

Use it to turn "Sprint 11" into the sprint id that create_work_package/update_work_package consume via their sprint parameter, or to review a project's sprint plan with its in_planning/active/completed states.

Returns the standard list envelope: items of {id, name, description, start_date, finish_date, workspace, status} plus pagination and notes. A project-scoped listing is fetched in full, so has_more is false; the instance-wide listing is paginated.

Pitfalls: sprints are not versions (17.3+): finish_date is the wire finishDate and status is the sprint lifecycle. Assign work with update_work_package(sprint=...); the API has no sprint write endpoint.

Cross-references: get_sprint for one sprint in full; list_versions for releases and milestones; list_work_packages with sprint_ids to see what is IN a sprint.

get_sprintA

Read one sprint in full: dates, workspace and lifecycle status.

Use it once list_sprints has given you an id and the row is not enough — this adds created_at/updated_at to the same shape.

Pitfalls: sprint ids and version ids are different id spaces — a version id from list_versions does not resolve here.

Cross-references: list_sprints for the id; list_work_packages with sprint_ids for the work planned in this sprint.

search_principalsA

Find users, groups and placeholder users, and get their ids.

This is the id-producing tool for every principal parameter in this server: assignee/responsible on work packages, watcher ids, the user_id of a time entry, and principal_id for create_membership (admin-gated: hidden unless the server sets OPENPROJECT_MCP_ADMIN_TOOLS=1). Names are never accepted where an id is wanted — resolve here first, and never guess a numeric id.

Use it to answer "who is Grace Hopper's account", "which groups exist", "who is a member of the demo project". Returns the standard list envelope: items of {id, name, type, email?, login?, status?} plus pagination{total,page,page_size,has_more} and notes.

Pitfalls. email, login and status are only returned for user principals the authenticated account may see — a null email means "not visible to you", not "no email". Group principals can hold memberships and be assigned work, so filter by type when you specifically need a person. Matching is substring-based, so a short query matches broadly; prefer the full name or the login. member_of_project filters by membership, not by whether the person ever touched the project.

Cross-references: get_user for one user's full detail; list_memberships for who holds which roles in a project; create_membership to grant access; list_roles for the role ids that grant needs.

get_userA

Read one user's profile: name, login, email, admin flag and status.

Use it after search_principals when you need more than a name — to confirm an account is active before assigning work, to check whether somebody is an instance administrator, or to learn which account the server itself is acting as (id_or_me='me').

Returns {id, name, login, email, admin, status, language, created_at, updated_at}. The avatar URL is deliberately dropped: it costs tokens and cannot be rendered here.

Pitfalls. email, login and admin are visibility-dependent — a null means the authenticated account may not see that field, never that the value is empty. admin=true says nothing about project permissions; use list_permissions for what the current user may actually do. A group or placeholder-user id returns 404 from this endpoint; those principals only appear in search_principals.

Cross-references: search_principals to find the id; list_memberships(principal_id=...) for the projects and roles this person holds; get_instance_info also reports the current user.

list_membershipsA

List who has access to which project, and with which roles.

Use it before granting or revoking access ("does she already have a role here?"), to audit a project's member list, or to see which projects a principal can reach. Called with no arguments it pages through every membership the authenticated account may see, which on a large instance is a lot — filter.

Returns the standard list envelope: items of {id, project, principal {id,name,type}, roles[], created_at, updated_at} plus pagination{total,page,page_size,has_more}.

Pitfalls. The id in each row is the membership id — the handle for update_membership and delete_membership — not the principal id and not the project id; mixing them up revokes the wrong access. A person can also reach a project through a group membership, so an empty result for principal_id does not prove they have no access. Memberships say who may act, not what they may do: roles carry the permissions.

Cross-references: list_roles for role ids and their permissions; create_membership / update_membership / delete_membership to change access (admin-gated: hidden unless the server sets OPENPROJECT_MCP_ADMIN_TOOLS=1); search_principals for principal ids; list_permissions for what the current user may do.

list_rolesA

List the roles this instance defines, with their ids.

This is the id-producing tool for create_membership.role_ids and update_membership.role_ids (both admin-gated: hidden unless the server sets OPENPROJECT_MCP_ADMIN_TOOLS=1) — role names are never accepted there. Roles are instance-wide definitions ('Member', 'Reader', 'Project admin'); a membership binds one principal to one project with a set of them.

Returns the standard list envelope with has_more: false: the role list is small and fetched in full. Each item is {id, name}, plus permissions when include_permissions=true.

Pitfalls. Role names are configurable per instance, so do not assume 'Member' exists — read the list. Some roles are not assignable to a project membership (global and work-package roles live in the same collection); the membership form rejects those with the assignable set listed. Not every OpenProject version exposes permission arrays on this endpoint: when include_permissions=true returns none, notes says so rather than pretending the roles grant nothing.

Cross-references: create_membership / update_membership consume these ids; list_memberships shows which roles are in use; list_permissions answers what the current user may do, which is the more useful question when a call just failed with 403.

get_instance_infoA

Check the OpenProject connection and report what this instance supports.

Call this first when anything fails in an unexplained way, when the user asks "am I connected / who am I", or before using a version-gated parameter. It is the server's connection test: it authenticates on every call rather than answering from cache.

Returns the core version and instance name, the attachment size ceiling (maximum_attachment_file_size_bytes), the page sizes the instance offers, the authenticated user {id, name, login, admin}, and features — the probe result telling you whether internal comments, emoji reactions and project favorites exist here, and which time-entry filter spelling this version uses.

Pitfalls: features describes the server version, not this user's permissions — a supported feature can still 403. A failure here is the actionable one: 401 means the API key is wrong or revoked, a network error means the URL, DNS, proxy or TLS trust is wrong; both come back with a hint naming the environment variable to fix.

For per-project ids (types, statuses, priorities, versions, categories, activities) use get_project_metadata; for what the current user may do use list_permissions.

get_project_metadataA

List the ids and names that are actually valid on this instance.

This is the one-call answer to "what do I pass for type / status / priority / version / category / activity". Call it before any create or update, before filtering by ids, and whenever a write fails with an allowed-values error. Nothing here is hardcoded — priority ids and activity ids differ per instance.

Without project_id returns the global types, statuses, priorities and roles. With project_id the types list narrows to the ones enabled in that project and versions, categories and time_entry_activities are filled in. Every row is {id, name} plus its flags: statuses[].is_closed is the authoritative done marker (never classify by status name — it is localized), types[].is_milestone tells you the type takes a single date, and priorities[].is_default / time_entry_activities[].is_default say what you get by omitting the field.

Pitfalls: results are cached (default 300 s) — pass refresh=true after an admin change. Time-entry activities are read from the time-entry form, so if the time tracking module is off or you lack permission the list comes back empty with a note in notes rather than an error (check notes).

For the writable fields and custom fields of one project+type combination use get_work_package_schema; for project ids themselves use list_projects.

get_work_package_schemaA

Show which fields a work package of this type accepts in this project.

Call it before create_work_package/update_work_package when you need the required fields, when you want a custom field's key or its allowed options, or after a 422 that named a field you do not recognise.

Returns required_fields (writable keys you must supply), fields — every core attribute with {key, name, type, required, writable, has_default, allowed_values} — and custom_fields with {key, name, type, required, writable, options}. allowed_values/options are {id, name} lists for status, category, version and list/user custom fields.

Pitfalls: key is the wire spelling (startDate, customField12) — that is what raw_filters and custom_fields writes use, though writes also accept the display name. A field with writable: false is computed by OpenProject; sending it is an error, not a no-op. allowed_values is null when the API only offers a lookup URL (assignee, project) — resolve those with search_principals or list_projects instead. Long option lists are capped at 50 with a marker in notes.

Ids for both parameters come from get_project_metadata; to read the values actually set on one work package use get_work_package.

list_permissionsA

List what the authenticated user is allowed to do, globally or in one project.

Use it before attempting a write that might 403, to explain to a user why an action failed, or to pick between tools ("can I add a member here, or should I ask an admin?"). It reads the real capabilities API for the current user — it does not return a user profile and it never guesses from the admin flag.

Returns the standard list envelope whose items are one row per context — {id, context, project, actions} with actions such as work_packages/create — plus principal (the user asked about), capability_count, and check when permission was given.

CAVEAT, straight from the API: OpenProject exposes only a SUBSET of its permissions as capabilities. An action missing from this list is not proof that the user lacks the permission — it may simply not be modelled. Treat a hit as reliable and a miss as "unknown, try it and read the 403".

Pitfalls: the capabilities API has no "me" value, so the numeric id of the authenticated user is resolved first (from the cached users/me) — you cannot ask about another user with this tool. Results are capped at 500 capabilities with a note in notes when the cap is hit; scope with project_id to stay well under it. Capabilities are about permission only: a permitted action can still fail validation.

Cross-references: get_instance_info reports who this server is authenticated as and what the instance version supports; list_memberships and list_roles show where the permissions come from; get_project_metadata lists the ids a permitted action needs.

enable_tool_groupA

Re-enable a tool group hidden by OPENPROJECT_MCP_PROFILE=core, for this session.

Use it when the user needs meetings, wiki, news, documents, budgets, git activity or reporting and those tools are not in your list. It never calls OpenProject.

Returns {group, title, status, tools_enabled, notes}; status is 'enabled' or 'already_visible'. Afterwards the server sends tools/list_changed: re-read your tool list before calling the new tools.

Pitfalls: it only lifts the core profile. It cannot override read-only mode (write tools stay hidden, see notes), the admin gate, or groups the operator removed with OPENPROJECT_MCP_DISABLE, which fail with invalid_input. Other sessions are unaffected.

Cross-references: get_instance_info shows which modules this instance supports.

Prompts

Interactive templates invoked by user choice

NameDescription
triage_inboxGroup the current user's unread OpenProject notifications by reason and suggest what to do with each group. Reads the unread inbox server-side and renders one section per reason (mentioned, assigned, watched, …) with the work packages behind it, so triage is a single pass instead of one notification at a time.
groom_backlogSweep a project's open backlog for the three things that rot it: work with no assignee, work with no estimate, and work nobody has touched in weeks. The open set is read oldest-changed first and classified here, because "has no estimate" is not expressible as an OpenProject filter. Counts always come from the server, so a capped scan understates the lists but never the backlog.

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4.3/5.0

Scored across 59 tools

Disambiguation4/5

Most tools target a distinct resource+action, and descriptions explicitly cross-reference alternatives (search_work_packages vs list_work_packages vs run_query; list_versions vs list_sprints; get_project_metadata vs get_work_package_schema). A few pairs remain easy to confuse at a glance—sprints versus versions, saved queries versus ad-hoc filters, and project phase definitions versus project phases—but the descriptions resolve them.

Naming Consistency5/5

All 59 tools use snake_case verb_noun names (get_work_package, update_project, list_time_entries, delete_attachment, mark_notifications), with predictable list/get/create/update/delete patterns across domains. Minor outliers like run_query, save_query, log_time and toggle_comment_reaction still follow the verb_noun convention.

Tool Count2/5

At 59 exposed tools this is a very heavy surface for an agent to navigate, spanning work packages, comments, watchers, relations, reminders, reactions, attachments, file links, projects, phases, queries, notifications, time entries, versions, sprints, principals, roles, memberships and metadata. The breadth is justified by OpenProject's domain, but 59 tools exceeds a comfortable selection budget and risks misselection.

Completeness4/5

Core lifecycle coverage is strong: work packages, projects, time entries, versions and comments each have full CRUD, with supporting metadata, schema, permission, job-status and notification tools. Gaps such as no edit/delete for saved queries, read-only file links and sprints, and admin-gated membership writes are explicitly documented but still leave a few dead ends.

Maintenance

ActivityActive
ResponsivenessNo issues