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.

Use it for every "what is assigned to me", "what is overdue", "what is in this sprint" question. Convenience queries are parameters here, 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: compact rows plus pagination, plus groups when group_by was requested and sums when show_sums was requested. Groups and sums are computed server-side over the whole filtered set, independent of paging — never re-add them from the rows on one page.

Pitfalls: this returns open work packages only unless you pass status_scope or status_ids, so say so when you report counts. status_ids overrides status_scope. Status, type, priority and version ids differ per instance and must come from get_project_metadata, never from memory.

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 is what makes failures useful: an invalid status, a missing required custom field or a type the project does not enable comes back 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, but versions, assignees and parents need numeric ids. Milestone types reject start_date/due_date — use date. Custom fields must exist on the project/type 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. Every convenience the old tooling spread across a dozen tools is a parameter here.

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, 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, decide, and retry deliberately rather than blindly. Status changes are validated against the workflow, so an invalid transition lists the allowed targets.

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

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.

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.

get_work_package_git_activityA

Show the code behind a work package: commits, pull/merge requests and CI status.

Use this for "is this ticket implemented", "what shipped for it", "did CI pass", "which branch/PR is this in". It returns, in one call: revisions (commits whose message references the work package, with full SHA, short SHA, author, message and commit time), github_pull_requests (title, state, draft, merged/merged_at, labels, author, URL and the CI check_runs with status and conclusion), gitlab_merge_requests (the same, with pipelines instead of check runs) and gitlab_issues.

available says, per source, whether this instance and this account can answer at all, and notes explains every false — "module absent" and "no permission" are different answers and neither means "no code was written". Report the notes rather than concluding a ticket has no development activity.

Pitfalls. Every pull/merge request carries two numbers: id is the OpenProject-internal id (the only thing get_github_pull_request accepts) and number is the '#481' humans quote on GitHub/GitLab. A state of 'closed' does not mean merged — check merged. A source that 403s or 404s is reported in notes, not raised, so a missing GitLab module never hides GitHub results.

Nothing appears here by magic. Links are created by text, not by the API: a commit message must mention the work package ('refs #123', or 'fixes #123' / 'closes #123' to also close it), and a pull or merge request must mention 'OP#123' or the full work-package URL in its description or a comment. OpenProject cannot browse repositories, list branches or diffs, or create these links through the API.

Cross-references: full pull-request detail (body, diff counts, all check runs) via get_github_pull_request(github_pull_request_id=<the id field>); the ticket itself via get_work_package; the discussion via list_work_package_comments.

get_github_pull_requestA

Read one linked GitHub pull request in full, including its CI check runs.

Use it after get_work_package_git_activity when the summary is not enough: this adds the pull-request body (markdown), the diff size (additions, deletions, changed_files), comment counts, who merged it, and every work package the PR is linked to — plus the same check_runs with status and conclusion.

Pitfalls. github_pull_request_id is OpenProject's id, never the GitHub number; the two are unrelated and there is no lookup by GitHub number. The record is a mirror that OpenProject refreshes from GitHub webhooks, so updated_at is when OpenProject last synced, not when GitHub changed. A 404 usually means the id came from the wrong field or the GitHub module is not installed on this instance.

A pull request appears in OpenProject only when its description or a comment mentions 'OP#123' or the full work-package URL; commits link separately via 'refs #123' in the commit message. Neither link can be created through the API.

Cross-references: find the id with get_work_package_git_activity(work_package_id=…); GitLab merge requests have no per-id tool — they come back in full from that same call.

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.

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 the whole team can open.

Use it when a filter combination is worth keeping — "Overdue in Platform", "My open bugs" — instead of rebuilding it every session: the saved view shows up in the OpenProject UI as well, and run_query reproduces it exactly. Prove the filters with list_work_packages first; whatever works there works here.

The call runs POST /queries/form before committing, so an invalid filter name, an operator the filter does not support, or a 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: it is what run_query takes.

Pitfalls. Filter values are ids, not names — 'Grace Hopper' is not a value, 12 is. star=true is a second request after the query exists; if it fails the query is still saved and notes says so, so never re-save on a starring failure. If OpenProject keeps fewer filters than were sent, notes says that too — read filters rather than assuming the view matches the request. Custom-field filters (customField12) are sent as plain values because a list-typed one cannot be told apart from a text one without asking the instance; if such a filter makes the call fail, nothing was saved — save that view in the UI. Editing and deleting saved views is deliberately not offered here: change or remove them in the OpenProject UI.

Cross-references: list_queries lists what already exists (and gives ids); run_query(query_id=...) runs this view; list_work_packages is the ad-hoc equivalent and the place to validate 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.

Use it to answer "how much time went into this ticket?", "what did I book last week?" 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 one groups bucket per activity with its own count and sums.total_hours — those cover the whole filtered set, so never add pages up yourself.

Pitfalls. Visibility is permission-bound: without the view-all-time-entries permission you see only your own entries, and 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, so a parent's roll-up needs a query per child. The summing path stops at 2000 entries and says so in notes; narrow the date range or the project when that happens rather than trusting the number.

Cross-references: book time with log_time; correct an entry with update_time_entry and remove one with delete_time_entry; the activity ids and names valid in a project come from get_project_metadata; the work package itself (including its aggregated spent_hours) comes 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.

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.

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. 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); 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 — 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.

list_meetingsA

List meetings — the schedule side of a project: what is coming up, what already ran.

Use it to answer "when do we next meet", "what meetings does this project have", or to find the meeting id that get_meeting and add_meeting_agenda_item need. Meeting ids are instance-wide and never guessable, so this is the way to get one.

Returns the standard list envelope: rows of {id, title, project, start_time, end_time, duration_hours, location, state} plus pagination and notes. Times are ISO 8601 UTC and duration_hours is a float (1.5 = 90 minutes). Rows are ordered by start time — ascending when upcoming_only, descending otherwise.

Pitfalls. Cancelled meetings and recurring-series templates are excluded upstream, so an absent meeting may exist in another state. state of 'draft' means the meeting has not been opened to its participants yet. Agenda items and participants are NOT in these rows; get_meeting fetches them. Meetings are a module: when it is not installed here or not enabled in the project (404), or this account may not read meetings (403), the call still SUCCEEDS with an empty items and the reason in notes — read notes before saying a project has no meetings, because an empty list with a note is not an empty schedule.

Cross-references: get_meeting(meeting_id=...) for participants, agenda and outcomes; create_meeting to schedule one; list_projects for the project id.

get_meetingA

Read one meeting in full: participants, the agenda, and any recorded outcomes.

This is the "what was discussed / what was decided" call. It returns the meeting fields (title, project, start_time, end_time, duration_hours, location, state, author, timestamps), the invited participants as {id, name} refs, and agenda_items in agenda order — each with its title, notes (markdown), duration_minutes, presenter, the work_package it discusses, its section, and the outcomes recorded against it (kind, notes, author, linked work package).

Pitfalls. A work-package agenda item carries an empty title — the work package's subject is what the UI shows, so read work_package.name. When the linked work package is invisible to this account, work_package is null and notes says so; do not report the item as unlinked. If the agenda itself cannot be read (403/404 on the sub-resource), agenda_items is empty and notes explains why — an empty agenda and an unreadable one are different answers. Attendance, minutes as a document, and meeting sections' own titles beyond the item link are not exposed by API v3.

Cross-references: add_meeting_agenda_item to extend the agenda; list_meetings for the id; list_attachments(container_type='meeting', container_id=<meeting id>) for files; get_work_package for a linked ticket. update_meeting / delete_meeting change or remove the meeting itself — the result's lock_version is what update_meeting echoes.

create_meetingA

Schedule a meeting in a project and optionally invite participants.

Use it for "book a review on Thursday" style requests. The call goes through POST /meetings/form first, so a missing permission, an impossible time or a participant who cannot see the project comes back as violations naming the attribute instead of an opaque rejection.

Returns the created meeting in the same shape as get_meeting (its agenda_items are empty — add them with add_meeting_agenda_item).

Pitfalls. Check state in the result: current OpenProject versions create meetings as 'draft', which means participants do not see it until it is opened — update_meeting(meeting_id=..., state='open') publishes it, exactly as the UI does. Invitation emails are not sent by an API create. start_time needs a timezone — the server stores an instant, not a wall-clock time. Recurring meetings cannot be created through this tool — use create_recurring_meeting.

Cross-references: add_meeting_agenda_item(meeting_id=...) to build the agenda; get_meeting to read it back; list_projects for the project id; search_principals (or list_project_memberships) for participant ids.

update_meetingA

Change a meeting's title, time, place or invite list — or move its lifecycle state.

Use it to reschedule ("move Thursday's review to 15:00"), to publish a draft (state='open'), to start or wrap up a running one (state='in_progress' / 'closed' — outcomes can only be recorded while it is in progress), or to fix the participants. Only the parameters you pass are sent; omitted fields stay as they are.

Returns the updated meeting in the same shape as get_meeting, including the fresh lock_version for a follow-up edit.

Pitfalls. participants replaces the entire set — a partial list silently uninvites everyone else. A closed meeting accepts a state-only patch (reopening it) and nothing else; any other change is rejected with a validation error until it is reopened. A conflict error (409) means somebody edited the meeting since you read it — the error carries the fresh lock_version and the differing fields, so re-read, decide, retry deliberately. This needs the 'edit meetings' permission, and moving a meeting to another project is deliberately not offered.

Cross-references: get_meeting for the current values and the lock_version; create_meeting to schedule a new one; delete_meeting to remove one; add_meeting_outcome for what an 'in_progress' state unlocks.

delete_meetingA

Permanently delete a meeting, together with its agenda and recorded outcomes.

Use only on explicit user instruction. The meeting, its agenda items, their outcomes and its attachments all go with it, and OpenProject offers no API-side undo. If the meeting merely did not happen, update_meeting(meeting_id=..., state='cancelled') is the reversible alternative that keeps the record.

Returns a small confirmation object once OpenProject accepts the deletion.

Pitfalls. This needs the 'delete meetings' permission, so a 403 is about the account, not the id. A 404 means the id is wrong, the meeting was already deleted, or — on OpenProject before 17.4 — the meetings write API does not exist at all; the hint names all readings.

Cross-references: get_meeting to check what you are about to destroy; update_meeting(state='cancelled') for the reversible alternative; list_meetings for the id.

add_meeting_agenda_itemA

Add one item to a meeting's agenda, optionally pinned to a work package.

Use it to build or extend an agenda: "add 'Release readiness' with 15 minutes", or "put #1234 on Thursday's agenda". Linking a work package is also how a ticket learns it was discussed — the link shows up on the work package's Meetings tab.

Returns the created item: {id, title, notes, duration_minutes, position, item_type, presenter, work_package, section, outcomes, meeting, created_at}. position is where it landed in the agenda; items are appended to the meeting's last section.

Pitfalls. This needs the 'manage agendas' permission, so a 403 is about the account, not the payload. A 422 usually means the work package is not visible to this account or the meeting is already closed — violations names the attribute. The item's type is fixed here: a simple item cannot become a work-package one later, because itemType is create-only upstream.

Cross-references: get_meeting(meeting_id=...) to see the agenda you are appending to; update_meeting_agenda_item to fix or reorder the item afterwards; delete_meeting_agenda_item to remove it; add_meeting_outcome to record a decision against it; list_meetings for the meeting id; search_work_packages for the work package id.

update_meeting_agenda_itemA

Edit one agenda item: retitle it, rewrite its notes, retime, reorder or re-link it.

Use it for "give that item 20 minutes", "move it to the top" (position=1), "let Grace present it", or to fix a wrong work-package link. Only the parameters you pass are sent; everything else is left exactly as it is.

Returns the updated item in the same shape as add_meeting_agenda_item, including the fresh lock_version for a follow-up edit.

Pitfalls. Once the meeting is CLOSED its agenda is frozen — every write answers a validation error until the meeting is reopened with update_meeting(state='open'). The item's type is fixed at creation: itemType is create-only upstream, so this tool never sends it and a simple item stays a simple item. A 422 otherwise usually means the presenter or work package is not visible in the project — violations names the attribute. This needs the 'manage agendas' permission (403 otherwise), and a 409 means a concurrent edit — the error carries the fresh lock_version.

Cross-references: get_meeting for the item ids, current values and lock versions; delete_meeting_agenda_item to remove the item; add_meeting_outcome to record what was decided under it.

delete_meeting_agenda_itemA

Permanently delete one agenda item, with any outcomes recorded against it.

Use only on explicit user instruction. The item and its recorded outcomes disappear from the agenda for good; the items after it move up. The meeting itself is untouched.

Returns a small confirmation object once OpenProject accepts the deletion.

Pitfalls. A CLOSED meeting's agenda is frozen: the delete answers a validation error, not a 403, until the meeting is reopened with update_meeting(state='open'). This needs the 'manage agendas' permission. A 404 means the id is wrong, the item is already gone — or, on OpenProject before 17.6, that the flat agenda-item write routes do not exist; the hint names all readings.

Cross-references: get_meeting to check which item the id points at; update_meeting_agenda_item when the item only needs fixing, not removing.

add_meeting_outcomeA

Record an outcome — a decision, a note, a follow-up ticket — against an agenda item.

This is how minutes are written through the API: "decision: ship on Friday" becomes kind='decision' with the text in notes; linking the follow-up work package makes it kind='work_package'. Outcomes appear under their agenda item in get_meeting.

Returns the created outcome: {id, kind, notes, author, work_package, agenda_item}.

Pitfalls — the timing rule matters most. Outcomes can only be written while the meeting state is exactly 'in_progress': before that, and again once it is closed, every outcome write answers a validation error. Start the meeting with update_meeting(meeting_id=..., state='in_progress') first. Items in a backlog section refuse outcomes the same way. This needs the 'manage outcomes' permission, so a 403 is about the account, not the payload. On OpenProject before 17.6 there is no outcomes API at all — the 404 hint says so.

Cross-references: get_meeting for the agenda item id and to read the outcome back; update_meeting to put the meeting into 'in_progress'; update_meeting_outcome / delete_meeting_outcome to correct or remove it while the meeting still runs.

update_meeting_outcomeA

Correct a recorded outcome's kind, text or linked work package.

Use it while the meeting still runs: fix a typo in the minutes, upgrade an information note to a decision, or attach the follow-up ticket that was created after the fact. Only the parameters you pass are sent.

Returns the updated outcome in the same shape as add_meeting_outcome.

Pitfalls. The same timing rule as every outcome write: this only works while the meeting state is exactly 'in_progress' — once it is closed, the minutes are what they are, and the write answers a validation error. Outcomes carry no lock_version upstream, so there is nothing to echo and a simultaneous edit by somebody else is silently overwritten — read the outcome via get_meeting first. This needs the 'manage outcomes' permission (403 otherwise).

Cross-references: get_meeting for the outcome id and current text; add_meeting_outcome for the kind rules; delete_meeting_outcome to remove it.

delete_meeting_outcomeA

Permanently delete a recorded outcome from a running meeting's minutes.

Use only on explicit user instruction, for an outcome recorded by mistake or against the wrong item. If the text is merely wrong, update_meeting_outcome corrects it and keeps the record.

Returns a small confirmation object once OpenProject accepts the deletion.

Pitfalls. The outcome timing rule applies to deletion too: it only works while the meeting state is exactly 'in_progress' — a closed meeting's minutes are frozen, and the delete answers a validation error, not a 403. This needs the 'manage outcomes' permission. A 404 means the id is wrong, the outcome is already gone — or, on OpenProject before 17.6, that there is no outcomes API at all.

Cross-references: get_meeting to check which outcome the id points at; update_meeting_outcome for the reversible fix.

get_wiki_pageA

Read a wiki page's identity and project — NOT its content.

Two limits define this tool, and both must be passed on to the user rather than worked around. First, wiki_page_id comes from a wiki page URL the user supplies: API v3 has no wiki index and no wiki search, so there is no way to look a page up by title or to list a project's pages. Second, the page's CONTENT is not exposed by the API at all — the response carries only {id, title, project} plus the page's attachments, and notes repeats that in-band.

So: use it to confirm which page a URL points at, to get the project a page belongs to, and as the step before fetching its files. To read the text, ask the user to paste it or open the page in the browser.

Pitfalls. A 404 means the id is wrong, the page was deleted, or the wiki is disabled in that project — it does not mean the wiki is empty. Sub-pages, revisions, page history and wiki-page↔work-package links are not exposed either. Creating or editing wiki pages is not supported by this server.

Cross-references: list_attachments(container_type='wiki_page', container_id=<id>) lists the files on the page and download_attachment fetches one; get_project_metadata for what the project does expose.

list_documentsA

List the documents visible to you, across every project.

Documents are OpenProject's filing cabinet: a title, a description, and attached files. Use this to find a document id for get_document or for list_attachments(container_type='document', ...).

Returns the standard list envelope: rows of {id, title, project, created_at, updated_at} plus pagination. The description is deliberately left out of the rows — get_document returns it in full.

Pitfalls. This is instance-wide: the endpoint takes no project parameter here, so filter by reading project on the rows, and page through rather than assuming page one is everything (pagination.has_more says). Documents are a module: where it is not installed, or not enabled in any project you can see (404), or where this account may not read documents (403), the call still SUCCEEDS with an empty items and the reason in notes — an empty list with a note does not mean no documents exist, so read notes first. The files themselves are attachments, not part of these rows.

Cross-references: get_document(document_id=...) for the description; list_attachments(container_type='document', container_id=...) then download_attachment for the files.

get_documentA

Read one document with its full description text.

Use it after list_documents when the title is not enough: this adds description as markdown, alongside {id, title, project, created_at, updated_at}.

Pitfalls. The attached files are not part of this result — list them with list_attachments(container_type='document', container_id=<this id>). A document created with OpenProject's block editor keeps its rich content in a field API v3 does not render, so description can be empty for a document that clearly has text in the UI; say so rather than reporting the document as blank. Editing documents is not supported by this server.

Cross-references: list_documents for the id; download_attachment for the files.

list_budgetsA

List a project's budgets — their ids and names, which is all API v3 exposes.

Use it to see whether a project tracks budgets at all and to get a budget id, which is what list_attachments(container_type='budget', ...) consumes.

Returns the standard list envelope with rows of {id, subject}. The collection is fetched in full, so has_more is false.

Pitfalls — read before answering a money question. API v3's budget representer carries no amounts: planned costs, spent costs, labor/material breakdowns and the assigned work packages are simply not there. Do not infer them and do not present a budget row as financial data; point the user at the budget in the UI, or use get_project_report_data / list_time_entries for the effort side. Budgets are also a module: a 404 (not installed, or not enabled in this project) and a 403 (this account lacks 'view budgets') both come back as a SUCCESSFUL call with an empty items and the reason in notes. Neither means the project has no budgets, so check notes before answering — only an empty list with no notes means there are none.

Cross-references: list_projects for the project id; list_time_entries for logged effort; list_attachments(container_type='budget', container_id=...) for budget files.

list_recurring_meetingsA

List recurring meeting series — the repetition rules, not the individual meetings.

Use it to answer "what regular meetings do we have" and to find the series id that get_recurring_meeting, the occurrence tools and delete_recurring_meeting need. Series ids are their own id space: a series id is never a meeting id, and the weekly occurrences themselves show up in list_meetings, not here.

Returns the standard list envelope: rows of {id, title, project, start_time, time_zone, frequency, interval, monthly_day, monthly_ordinal, monthly_weekday, end_after, end_date, iterations, duration_hours, location} plus pagination and notes.

Pitfalls. The listing is instance-wide — the endpoint takes no project filter, so scope by reading project on the rows. duration_hours is per occurrence. Recurring meetings are 17.4+ AND a module: where the API predates them, the module is off, or this account may not read meetings, the call still SUCCEEDS with an empty items and the reason in notes — read notes before saying there are no recurring meetings.

Cross-references: get_recurring_meeting(recurring_meeting_id=...) for the schedule with its next occurrences; create_recurring_meeting to start a series; list_meetings for the instantiated occurrences.

get_recurring_meetingA

Read one recurring series in full: the schedule plus its next occurrences.

This is the step before touching any occurrence: the occurrences rows carry the exact start_time strings that init_recurring_meeting_occurrence and cancel_recurring_meeting_occurrence key on, and the meeting_id that get_meeting / delete_meeting take once a slot is instantiated.

Returns the series fields (schedule, duration_hours, location, author, template_meeting_id) plus occurrences in date order: {start_time, state, meeting_id}. state is 'planned' for a slot that exists only in the schedule; once instantiated it is the backing meeting's own state, and only then is meeting_id non-null.

Pitfalls. occurrences is capped at the next few slots — notes says when the cap was hit, and an unbounded series always computes more. The template meeting (its agenda seeds every occurrence) is edited through the regular meeting tools via template_meeting_id; while it is still a draft, occurrences cannot be initialized. A 404 means a wrong id, no 'view meetings' permission, the module is off — or OpenProject before 17.4, which has no recurring-meetings API; the hint names all readings.

Cross-references: list_recurring_meetings for the series id; init_recurring_meeting_occurrence / cancel_recurring_meeting_occurrence for one slot; update_meeting on the template to build the shared agenda or publish a draft template; delete_recurring_meeting to remove the whole series.

create_recurring_meetingA

Create a recurring meeting series: a schedule plus a template the occurrences copy.

Use it for "set up a weekly sync Mondays at 9" style requests. The frequency and end_after combinations are validated locally BEFORE anything is sent — OpenProject's own "infer the monthly fields" defaults never apply to API creates, so a bad combination is rejected here with the allowed matrix spelled out.

Returns the created series in the same shape as get_recurring_meeting, including the computed next occurrences (their start_time strings are what the occurrence tools take) and template_meeting_id.

Pitfalls — two upstream quirks are handled but must be understood. First, the template meeting is created as a DRAFT: notes says so, and occurrences cannot be initialized until update_meeting(meeting_id=<template_meeting_id>, state='open') publishes it. Second, OpenProject overwrites time_zone on create with the API account's own zone; this tool detects that and corrects it with a follow-up PATCH — if that correction is refused (it needs 'edit meetings'), the series is still created and notes names the zone it actually runs in. start_time must be now or in the future, or the create is rejected with a validation error.

Cross-references: get_recurring_meeting to read it back; update_meeting(meeting_id=<template_meeting_id>, ...) to build the shared agenda and publish the template; init_recurring_meeting_occurrence to materialize a slot; list_projects for the project id.

delete_recurring_meetingA

Permanently delete a recurring series: template, schedule, and EVERY occurrence.

Use only on explicit user instruction, and make sure the user means the whole series: every instantiated meeting of the series — past minutes included — is destroyed with the template, and when the series has notify set, participants are emailed cancellations. To drop a single slot instead, cancel_recurring_meeting_occurrence is the right tool, and delete_meeting removes one instantiated meeting.

Returns a small confirmation object once OpenProject accepts the deletion.

Pitfalls. This needs the 'delete meetings' permission, so a 403 is about the account, not the id. A 404 means the id is wrong, the series was already deleted — or OpenProject before 17.4, which has no recurring-meetings API at all; the hint names all readings.

Cross-references: get_recurring_meeting to check what you are about to destroy; cancel_recurring_meeting_occurrence for one slot; delete_meeting for one instantiated meeting.

init_recurring_meeting_occurrenceA

Materialize one occurrence of a series as a real meeting, copied from the template.

Use it when a specific slot needs its own agenda, minutes or attachments before the day: the occurrence becomes a normal meeting (template agenda and attachments copied) that every meeting tool can work on. Called on a cancelled occurrence it RESTORES it to 'open'; called where an open meeting already exists it idempotently returns that meeting.

Returns the instantiated meeting in the same shape as get_meeting — its id is the meeting id for follow-up calls, distinct from the series id.

Pitfalls — the instant is trusted, not validated. OpenProject matches start_time by timestamp equality and does NOT check it against the schedule, so a wrong instant creates a real off-schedule meeting: always copy the string from get_recurring_meeting's occurrences (offsets are normalized to UTC 'Z' form on the wire). An HTTP 500 here almost always means the series' template is still a DRAFT — OpenProject fails uncleanly on that instead of answering 422; publish the template with update_meeting(meeting_id=<template_meeting_id>, state='open') and retry. This needs the 'create meetings' permission (403 otherwise; OpenProject 17.4 itself briefly wanted 'edit meetings').

Cross-references: get_recurring_meeting for the exact start_time strings and the template id; update_meeting / add_meeting_agenda_item on the result; cancel_recurring_meeting_occurrence for the opposite move.

cancel_recurring_meeting_occurrenceA

Cancel one occurrence of a series — skip a slot without touching the schedule.

Use it for "no sync next Monday": the slot stays in the occurrences list as 'cancelled' (backed by a cancelled stub meeting) while the series keeps running. A cancelled occurrence is recoverable — init_recurring_meeting_occurrence at the same instant restores it to 'open' — but the cancellation itself may email participants when the series has notify set, which is why it is confirm-gated.

Returns a small confirmation object carrying the normalized instant.

Pitfalls. The instant is matched exactly and never validated against the schedule: cancelling at a wrong time succeeds (204) by creating a cancelled phantom stub while the real occurrence lives on — always copy start_time from get_recurring_meeting's occurrences. A conflict error (409) means the occurrence is already instantiated as a live meeting, which OpenProject refuses to cancel in place: delete_meeting(meeting_id=...) (the id is in the occurrences row) is the move then. Cancelling an already-cancelled slot is an idempotent success. This needs the 'edit meetings' permission.

Cross-references: get_recurring_meeting for the exact start_time and the occurrence's meeting_id; delete_meeting for an instantiated occurrence; delete_recurring_meeting to end the whole series.

list_newsA

List project news — the announcements a team publishes on its project overview.

Use it to answer "what was announced recently", to find the id of an entry before reading, editing or deleting it, or to check whether a report was already published. Results come back newest first (sorted by creation date descending).

Returns the standard list envelope: items of {id, title, summary, project, author, created_at, can_manage} plus pagination and notes. Rows carry the short summary only — the full markdown body is fetched per entry with get_news(news_id=...), which keeps a long announcement out of a listing. can_manage tells you in advance whether update_news/delete_news would be allowed for that row.

Pitfalls: news is only visible where the project has the news module enabled and this account holds the 'view news' permission, and neither absence is an error — an empty page carries a notes entry saying so, and reporting "this project has no announcements" without reading it would be wrong. The project scope is matched by numeric id; an identifier is resolved with one extra lookup, so an unknown identifier fails as not_found rather than silently listing the whole instance.

Cross-references: get_news for the full body of one entry; create_news to publish one; list_projects for project ids; work-package discussion lives in list_work_package_comments, not here.

get_newsA

Read one news entry in full, including the markdown body.

Use it after list_news when the summary is not enough — this adds description, the announcement's complete text as markdown (html is dropped), plus updated_at.

Returns {id, title, summary, description, project, author, created_at, updated_at, can_manage}. author is the account that published the entry and cannot be changed; can_manage says whether editing or deleting it would be permitted.

Pitfalls: a 404 here means "no such entry, or you may not read news in its project" — the news module is enabled per project, so a missing entry is not always a wrong id. Comments people left on the announcement are not exposed by API v3 and are not included.

Cross-references: list_news produces the id; update_news changes the text; delete_news removes the entry for good.

create_newsA

Publish a news announcement in a project.

Use it for release notes, a weekly report, a maintenance window — anything the whole project should see on its overview page. The author is the authenticated account and is set by the server; project members watching the project are notified.

Returns the created entry {id, title, summary, description, project, author, created_at, updated_at, can_manage}; the id is what update_news and delete_news consume.

Pitfalls: this needs the 'manage news' permission in that project, which exists only while the project has the news module enabled — a 403 is about the account or the module, never about the text. title is required and rejected when blank (checked here, before the request). There is no draft state: the entry is public to everyone who can view the project the moment it is created. News is not a work package — for something that needs assigning and tracking use create_work_package instead.

Cross-references: list_news to see what is already published (and to avoid duplicates); update_news to correct an entry afterwards; list_projects for the project id.

update_newsA

Correct or rewrite a published news entry.

Use it to fix a headline, refresh a weekly report in place, or clear a stale teaser. Only the parameters you pass are sent, so a concurrent edit to another field survives.

Returns the updated entry in the same shape as get_news.

Pitfalls: summary and description REPLACE the stored text — there is no append. The entry's project and author are fixed at creation and cannot be updated here; publish a new entry instead. News carries no lockVersion upstream, so there is nothing to echo and no lock parameter: a simultaneous edit by somebody else is silently overwritten, which is why reading with get_news first is worth it. The 'manage news' permission is required, so a 403 is about the account or a disabled news module.

Cross-references: get_news for the current text and for can_manage; delete_news when the entry should disappear entirely; create_news to publish a follow-up instead of rewriting history.

delete_newsA

Permanently delete a news entry.

Use it for an announcement published by mistake or in the wrong project. For an outdated but real announcement, update_news is usually the better answer: the entry stays part of the project's record.

Returns a small confirmation once OpenProject accepts the deletion.

Pitfalls: the entry and every comment left on it are removed for good — API v3 offers no undo and no trash. The 'manage news' permission is required, so a 403 is about the account or a disabled news module; a 404 means the id is wrong or its news is not visible to you.

Cross-references: list_news/get_news for the id and for can_manage; update_news for the reversible alternative.

get_project_report_dataA

Aggregate everything a status report needs about one project and one date window.

Use it for weekly reports, sprint reviews, standups and "what happened in June" — one call replaces a dozen filtered listings. It returns, for the window: created, updated and closed work-package buckets (each {items, total, truncated, more_via} with compact rows), open_total plus open_by_status counts computed server-side over the whole open set, a time summary (total hours with per-activity and per-user breakdowns) and the project's membership roster.

Done/in-progress classification is safe here: every row carries is_closed, read from the status's own isClosed flag on this instance, so it works on translated and renamed workflows where matching status names would not. closed is exactly "in a closed status and touched inside the window" — the done-this-week set.

Pitfalls. Counts and row lists are different things: total is always the server's number, while items stops at an internal cap and then sets truncated and adds a notes entry — quote the count, not the row count. open_by_status covers the open set as it is now, not as it was during the window. updated includes the rows in closed. Time visibility is permission-bound, so a total_hours of 0 can mean "not allowed to see" rather than "nobody logged time" — an unreadable time ledger and an unreadable roster each degrade into a notes entry instead of failing the call. Read notes before calling any number complete.

Cross-references: rendered reports are the weekly_report and daily_standup prompts, which run this same aggregation server-side; drill into a bucket with list_work_packages, into hours with list_time_entries, and into one row with get_work_package.

Prompts

Interactive templates invoked by user choice

NameDescription
weekly_reportRender the 8-section Agile/Scrum weekly report for one project, with live data. The server does the reading: work packages created, changed and completed in the window, the server-side open-by-status counts, logged hours per activity and per person, the membership roster, and the blocking relations on open work. Done is decided by each status's own `isClosed` flag, so the report is correct on translated and renamed workflows; the rest split into Planned (raised in the window and untouched since) and In progress.
daily_standupRender today's standup for one project: yesterday's movement, what is due today, and what is blocked. The window is yesterday on the server's clock. Completed items are the ones whose status carries the instance's `isClosed` flag, never a status name; "due today" is an open-status query on today's date; blockers are the `blocks`/`blocked` relations visible on the open work packages that moved.
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

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kar-thik/openproject-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server