Skip to main content
Glama

OpenProject MCP Server

PyPI CI openproject-mcp MCP server

An MCP (Model Context Protocol) server for the OpenProject API v3. It gives Claude and any other MCP client 91 tools covering work packages, comments and relations, attachments, git/PR activity, projects, saved queries, notifications, time tracking, versions, people and memberships, meetings, news, documents, budgets and reporting — plus 4 report/workflow prompts and 3 resource templates. Built on FastMCP 4.x and httpx (HTTP/2); speaks MCP 2026-07-28 and the earlier handshake versions.

This project is aimed at the OpenProject Community edition. OpenProject's Enterprise edition now ships with its own built-in MCP integration; this server brings the same capability to self-hosted Community instances. It runs fine against any edition — it only needs the public API v3.

Design principles, all enforced in code:

  • Structured everything. Every tool returns a typed model, so clients get an outputSchema and machine-readable structuredContent, not prose. Errors come back as a JSON envelope with a stable type, the upstream http_status, a message and a hint describing how to correct the call.

  • Honest degradation. OpenProject instances differ by version, installed modules and permissions. Tools report what they could not see as in-band notes — a missing module yields an empty page with an explanation, never a fake success or a bare traceback.

  • Safe by default. A read-only mode, admin-gated membership writes, per-group tool disabling, a confirm=true guard on every destructive tool, TLS always verified, and credentials that never appear in logs.

  • Version-adaptive. Targets OpenProject 14 LTS through 17.x; API differences are detected by a lazy, cached feature probe instead of assumptions (see Supported OpenProject versions).

What it looks like

Two exchanges against a fictional "Website Relaunch" project — every id, name and date below is invented. You describe the outcome; the model picks the tools, and what comes back to it is a typed object rather than prose to re-parse.

You         What is still open in Website Relaunch and due by Friday? The checkout
            bug is urgent — raise it and leave a note for whoever owns it.

  →  list_work_packages(project="website-relaunch", status_scope="open",
                        due_before="2026-03-13", sort_by=[["due_date", "asc"]])

     { "items": [
         { "id": 4821, "subject": "Checkout fails on expired coupon codes",
           "type": {"id": 7, "name": "Bug"}, "status": {"id": 7, "name": "In progress"},
           "assignee": {"id": 118, "name": "Dana Okafor"}, "due_date": "2026-03-11" },
         { "id": 4830, "subject": "Move press-kit assets to the new CDN",
           "type": {"id": 1, "name": "Task"}, "status": {"id": 1, "name": "New"},
           "assignee": null, "due_date": "2026-03-13" }],
       "pagination": {"total": 2, "page": 1, "page_size": 20, "has_more": false} }

  →  update_work_package(id=4821, priority="High")
  →  add_work_package_comment(id=4821, comment="Raised to High — it blocks the
                              launch checklist. Dana, can you take a look today?")

Assistant   Two open items are due by Friday. #4821 is High now and the note is on
            its thread; #4830 is due Friday and still has nobody on it.

The rows above are abridged: each one also carries priority, project, start_date, percentage_done and updated_at, and the envelope keeps groups, sums and notes slots for the calls that fill them.

The prompt templates cut out the tool round trips: the server reads OpenProject itself and hands back a finished document rather than instructions to go and fetch one. It arrives prefaced with a note that the numbers are already computed and must not be recomputed or embellished.

You         /weekly_report project=website-relaunch

            # WEEKLY REPORT - AGILE SCRUM

            _Generated from live OpenProject data_
            ...
            ## B. EXECUTIVE SUMMARY

            **Progress against the sprint goal:** Not assessed — sprint goal and completion dates are not available

            **Currently closed work updated in the window:**
            1. #4802 - Ship the new pricing page
            2. #4795 - Halve the hero image payload
            ...
            ### Data notes

            - open/closed bucketing uses each status's isClosed flag from
              GET /statuses, not status names; a status this instance renamed
              or translated is still bucketed correctly

That last block is the house style: a report says which of its numbers are partial, and a tool that could not read something returns the gap as a note instead of guessing. The whole surface is in Tools and Prompts and resources.

Related MCP server: MCP Toolbox for Databases

Requirements

  • Python >= 3.12

  • An OpenProject instance, version 14 LTS through 17.x (any edition; aimed at Community — Enterprise ships its own MCP integration)

  • An OpenProject API key: in OpenProject, go to My account → Access tokens and generate an API token

Installation

The distribution name is openproject-mcp-server. It installs two identical console scripts, openproject-mcp-server and openproject-mcp; the long form is canonical (an unrelated PyPI package also installs a bin named openproject-mcp).

Run one-shot with uv, no install step:

uvx openproject-mcp-server

Or install persistently:

uv tool install openproject-mcp-server
# or
pip install openproject-mcp-server

The minimal configuration is two environment variables:

export OPENPROJECT_URL=https://openproject.example.com
export OPENPROJECT_API_KEY=your-api-key

Validate the configuration without starting the server:

openproject-mcp-server --check

--check verifies the configuration and exits; it does not contact your instance. Once connected through a client, call the get_instance_info tool for a live end-to-end check. When configuration is missing or invalid, the server prints the specific problems to stderr and exits with code 2 — never a traceback.

Claude Code

claude mcp add openproject \
  --env OPENPROJECT_URL=https://openproject.example.com \
  --env OPENPROJECT_API_KEY=your-api-key \
  -- uvx openproject-mcp-server

Claude Desktop and other MCP clients

Add to claude_desktop_config.json (or your client's equivalent mcpServers config):

{
  "mcpServers": {
    "openproject": {
      "command": "uvx",
      "args": ["openproject-mcp-server"],
      "env": {
        "OPENPROJECT_URL": "https://openproject.example.com",
        "OPENPROJECT_API_KEY": "your-api-key"
      }
    }
  }
}

From source

git clone https://github.com/kar-thik/openproject-mcp
cd openproject-mcp
uv sync
uv run openproject-mcp-server

Updating or rotating your API token

When a token is regenerated, revoked or invalidated (OpenProject major upgrades can do this — the symptom is every tool suddenly failing with authentication_failed / HTTP 401), generate a fresh one in OpenProject under My account → Access tokens and update it wherever your key lives:

  • Shell environment (easiest to rotate). The server reads OPENPROJECT_API_KEY straight from the OS environment, so you can export it globally — e.g. in ~/.zshenv — and register the server with no --env flags at all:

    claude mcp add openproject -- uvx openproject-mcp-server

    Rotation is then: edit the export, open a fresh terminal, reconnect. The client config never contains a secret. (This does not work for GUI apps like Claude Desktop, which don't read your shell profile.)

  • Claude Code with --env. The registration stores the key, so replace it:

    claude mcp remove openproject
    claude mcp add openproject \
      --env OPENPROJECT_URL=https://openproject.example.com \
      --env OPENPROJECT_API_KEY=new-key \
      -- uvx openproject-mcp-server

    Then reconnect via /mcp (a running session keeps the old environment until it does).

  • Claude Desktop and other JSON-configured clients. Edit the OPENPROJECT_API_KEY value in the client config and restart the client.

  • .env file. Edit the file and restart the server.

Recent OpenProject versions allow several API tokens in parallel, so you can rotate with zero downtime: create the new token, switch your clients over, then revoke the old one.

Configuration

Configuration is entirely environment-driven. The table below is the authoritative reference: the server binds exactly these 26 names and no others. Bare, unprefixed names such as READ_TIMEOUT or API_KEY are deliberately ignored (a stray variable in your shell cannot change or break the server), as is any other unknown variable. A .env file in the server's working directory is read with the same names; real environment variables take precedence. From-source users can start from .env.example.

Variable

Default

Purpose

OPENPROJECT_URL

— (required)

Instance root URL, e.g. https://openproject.example.com. A trailing /api/v3 is tolerated and stripped.

OPENPROJECT_API_KEY

— (required*)

API key from My account → Access tokens. Sent as HTTP Basic apikey:<token>.

OPENPROJECT_OAUTH_TOKEN

unset

OAuth bearer token, as an alternative to the API key. *One of the two credentials is required.

OPENPROJECT_MCP_ACCEPT_LANGUAGE

unset

Sent as the Accept-Language header; OpenProject localizes validation messages accordingly.

OPENPROJECT_MCP_CA_BUNDLE

system trust store

Path to a CA bundle (PEM) for instances behind a private CA. TLS is always verified; there is deliberately no off switch.

OPENPROJECT_MCP_READ_ONLY

false

Serve read tools only: every write, destructive and admin tool is removed at startup.

OPENPROJECT_MCP_ADMIN_TOOLS

false

Expose the three admin-gated membership write tools (hidden by default).

OPENPROJECT_MCP_DISABLE

empty

Comma-separated group tags to remove whole tool groups at startup (see below).

OPENPROJECT_MCP_PROFILE

core

core hides the module-backed groups at startup; a client can bring one back with enable_tool_group (see below). full lists every group.

OPENPROJECT_MCP_INSECURE

false

Allow --transport http to start without auth tokens. Local development only.

OPENPROJECT_MCP_DOWNLOAD_DIR

./openproject-downloads

Directory where download_attachment writes files (created if missing; default is relative to the server's working directory).

OPENPROJECT_MCP_UPLOAD_ROOT

unset

When set, upload_attachment and create_work_package(attachment_paths) read only files beneath this existing directory; paths and symlinks resolving outside it are refused. Recommended for --transport http.

OPENPROJECT_MCP_DOWNLOAD_ROOT

unset

When set, download_attachment writes only beneath this existing directory and saves there by default; a save_dir outside it is refused. Recommended for --transport http.

OPENPROJECT_MCP_MAX_DOWNLOAD_MB

100

Size cap for attachment downloads, in MiB.

OPENPROJECT_MCP_MAX_INLINE_MB

10

Size cap for attachment bytes returned inline by the openproject://attachment resource, in MiB (never above the download cap).

OPENPROJECT_MCP_CACHE_TTL

300

TTL in seconds for the metadata cache (statuses, types, priorities, schemas).

OPENPROJECT_MCP_LOG_LEVEL

INFO

DEBUG, INFO, WARNING, ERROR or CRITICAL (case-insensitive).

OPENPROJECT_MCP_LOG_FORMAT

text

text or json. Logs always go to stderr (stdout belongs to the stdio transport).

OPENPROJECT_MCP_LOG_BODIES

false

Log request/response bodies — only at DEBUG level, with credentials redacted. Development use only.

OPENPROJECT_MCP_OTEL

false

Reserved for OpenTelemetry tracing. Accepted but not yet wired to anything in this release; setting it produces no traces.

OPENPROJECT_MCP_HTTP_HOST

127.0.0.1

Bind address for --transport http.

OPENPROJECT_MCP_HTTP_PORT

8000

Port for --transport http.

OPENPROJECT_MCP_AUTH_TOKENS

unset

Comma-separated bearer tokens accepted by --transport http (e.g. one per client). Every request must carry Authorization: Bearer <token> with one of them; anything else gets a 401. Without it (and without OPENPROJECT_MCP_INSECURE=1), the HTTP transport refuses to start. See Transports.

OPENPROJECT_MCP_CONNECT_TIMEOUT

10

Seconds to wait for a TCP/TLS connection to OpenProject.

OPENPROJECT_MCP_READ_TIMEOUT

30

Seconds to wait for response data.

OPENPROJECT_MCP_WRITE_TIMEOUT

60

Seconds to wait while sending request data (uploads).

OPENPROJECT_MCP_POOL_TIMEOUT

5

Seconds to wait for a free connection from the pool.

OPENPROJECT_MCP_MAX_CONNECTIONS

10

Connection pool size toward OpenProject.

OPENPROJECT_MCP_MAX_RETRIES

3

Retry budget for idempotent requests.

Secrets (OPENPROJECT_API_KEY, OPENPROJECT_OAUTH_TOKEN, OPENPROJECT_MCP_AUTH_TOKENS) are held in memory as Pydantic SecretStr values and are never written to logs; the Authorization header is redacted in every log record.

Limiting what the model can do

Four settings shrink the tool surface at startup. The tool list is then fixed for the lifetime of the process, with one exception: enable_tool_group can bring back a group hidden by the core profile, for the calling client (see below for what that means per protocol version).

  • OPENPROJECT_MCP_READ_ONLY=1 serves only the 40 read tools.

  • OPENPROJECT_MCP_ADMIN_TOOLS=1 reveals the three membership write tools (create_membership, update_membership, delete_membership); they are hidden by default.

  • OPENPROJECT_MCP_DISABLE drops whole groups to cut prompt cost, e.g. OPENPROJECT_MCP_DISABLE=meetings,news. The valid group tags are: work_packages, wp_collaboration, attachments, git_activity, projects, queries, notifications, time_entries, versions, people, metadata, meetings, wiki, documents, budgets, news, reporting — the same tags that head each section of the tool catalog below — plus meetings_recurring, a sub-tag of meetings that drops only the six recurring-meeting tools (meetings drops them too).

  • OPENPROJECT_MCP_PROFILE defaults to core (since 0.4.0), which hides the module-backed groups — meetings, news, documents, wiki, budgets, git_activity and reporting, plus the two reporting prompts — so a session starts with a smaller tool list. Set it to full to list every group from the start. When the model needs one of them it calls enable_tool_group, which restores that group and tells the client to re-read its tool list (if yours never does, set full). Who sees the restored group depends on the client's protocol version, and the result's scope field names it:

    • session — clients on the initialize handshake (MCP 2025-11-25 and earlier): that MCP session only. Over HTTP a session ends after 30 minutes idle.

    • token — MCP 2026-07-28 clients over HTTP, which have no session: every connection presenting the same bearer token. Give each user their own token in OPENPROJECT_MCP_AUTH_TOKENS if they should not share a tool list. With OPENPROJECT_MCP_INSECURE nothing identifies the client, so the call is refused; run full there (the server warns at startup).

    • process — MCP 2026-07-28 clients over stdio: the whole server process, which serves one client anyway.

    Enabled groups are forgotten after 24 hours idle. enable_tool_group never lifts READ_ONLY, the admin gate or OPENPROJECT_MCP_DISABLE; it lives in the metadata group, so disabling that group removes it.

Independent of all four, every destructive tool (the 13 permanent deletes) requires an explicit confirm=true argument before it acts.

Transports

  • stdio (default) — what Claude Code, Claude Desktop and most local MCP clients use. Logs go to stderr; stdout carries only the protocol.

  • Streamable HTTP — openproject-mcp-server --transport http. Binds 127.0.0.1:8000 by default; override with --host/--port or OPENPROJECT_MCP_HTTP_HOST/ OPENPROJECT_MCP_HTTP_PORT. The HTTP transport refuses to start without authentication configured: set OPENPROJECT_MCP_AUTH_TOKENS to a comma-separated list of bearer tokens, or — for local development only — set OPENPROJECT_MCP_INSECURE=1.

    With tokens configured, every HTTP request to the MCP endpoint must carry an Authorization: Bearer <token> header whose token matches one of the configured values (compared in constant time); requests with a missing, malformed or unknown token are rejected with a 401 and a WWW-Authenticate header. Multiple tokens are supported — for example one per client, so each can be revoked independently. Tokens must be ASCII, and the endpoint applies no rate limiting, so use long random values — for example python -c "import secrets; print(secrets.token_urlsafe(32))". OPENPROJECT_MCP_INSECURE=1 remains a development-only escape hatch that leaves the endpoint open without authentication.

    The server does not terminate TLS, so tokens would otherwise cross the network in plaintext: keep the default 127.0.0.1 bind, and if you must expose the HTTP transport beyond localhost, put it behind a TLS-terminating reverse proxy (for example nginx or Caddy).

    One upstream identity. Bearer tokens decide who may reach the endpoint, not who they are in OpenProject: the server holds a single OPENPROJECT_API_KEY (or OAuth token), and every HTTP client acts as that OpenProject user and shares its metadata cache. Deploy it for clients you would trust with that account, and issue a dedicated, least-privileged OpenProject user for it.

    File tools touch the server's disk. upload_attachment, create_work_package (attachment_paths) and download_attachment read and write paths on the machine running the server. Over HTTP they are off until you confine them: upload_attachment and attachment_paths need OPENPROJECT_MCP_UPLOAD_ROOT, download_attachment needs OPENPROJECT_MCP_DOWNLOAD_ROOT. Until then the tool is hidden (and attachment_paths is refused), and the server names the missing variable in a startup warning.

Tools

91 tools: 42 read, 46 write and 3 admin-gated writes. The admin tools stay hidden unless OPENPROJECT_MCP_ADMIN_TOOLS=1; the 13 destructive tools additionally require confirm=true on every call. Each section heading names the group tag accepted by OPENPROJECT_MCP_DISABLE.

Work packages (work_packages)

Tool

Kind

What it does

search_work_packages

Read

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

list_work_packages

Read

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

get_work_package

Read

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

create_work_package

Write

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

update_work_package

Write

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

bulk_update_work_packages

Write

Preview or apply up to 50 updates with per-item diffs, validation and conflict results.

delete_work_package

Write (destructive)

Permanently delete a work package and everything attached to it.

Batch workflow: call bulk_update_work_packages(updates=[{"id": 4821, "changes": {"target_versions": [3, 4]}}]) to preview. Review the returned changes, then repeat with dry_run=false and each item's returned lock_version included in updates. Any preflight error prevents the whole batch from writing. A later conflict can produce partial success; inspect every item and never replay successful updates. unknown means the write may have committed before the response was lost.

Comments, relations, watchers, reminders (wp_collaboration)

Tool

Kind

What it does

list_work_package_comments

Read

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

add_work_package_comment

Write

Post a comment on a work package.

edit_work_package_comment

Write

Rewrite the text of an existing work-package comment.

add_work_package_watcher

Write

Subscribe a user to a work package's notifications.

remove_work_package_watcher

Write

Unsubscribe a user from a work package's notifications.

create_work_package_relation

Write

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

update_work_package_relation

Write

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

delete_work_package_relation

Write (destructive)

Remove the link between two work packages.

toggle_comment_reaction

Write

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

set_work_package_reminder

Write

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

list_reminders

Read

List your own upcoming work-package reminders.

execute_custom_action

Write

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

Tool

Kind

What it does

list_attachments

Read

List the files attached to one container.

download_attachment

Read

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

upload_attachment

Write

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

delete_attachment

Write (destructive)

Permanently delete one attached file from OpenProject.

list_file_links

Read

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

Git and pull requests (git_activity)

Tool

Kind

What it does

get_work_package_git_activity

Read

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

get_github_pull_request

Read

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

Projects (projects)

Tool

Kind

What it does

list_projects

Read

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

get_project

Read

Read one project in full.

create_project

Write

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

update_project

Write

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

delete_project

Write (destructive)

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

copy_project

Write

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

get_job_status

Read

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

set_project_favorite

Write

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

list_project_phase_definitions

Read

List the instance-wide phase catalog (Initiating, Planning, ...) with start/finish gates (OpenProject 16.1+).

get_project_phase

Read

Read one project's phase record; ids come from a work package's project_phase reference.

Saved queries (queries)

Tool

Kind

What it does

list_queries

Read

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

run_query

Read

Run a saved view and get its work packages as they are right now.

save_query

Write

Save a filter set as a reusable OpenProject view the whole team can open.

Notifications (notifications)

Tool

Kind

What it does

list_notifications

Read

Read the authenticated user's OpenProject inbox.

mark_notifications

Write

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

mark_all_notifications_read

Write

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

Time tracking (time_entries)

Tool

Kind

What it does

list_time_entries

Read

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

log_time

Write

Book time against a work package or a project.

update_time_entry

Write

Correct an existing time entry.

delete_time_entry

Write (destructive)

Permanently delete a logged time entry.

Versions and sprints (versions)

Tool

Kind

What it does

list_versions

Read

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

list_sprints

Read

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

get_sprint

Read

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

create_version

Write

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

update_version

Write

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

delete_version

Write (destructive)

Permanently delete a version.

People and memberships (people)

Tool

Kind

What it does

search_principals

Read

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

get_user

Read

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

list_memberships

Read

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

create_membership

Admin write

Grant a principal one or more roles in a project.

update_membership

Admin write

Replace the roles of an existing membership.

delete_membership

Admin write (destructive)

Revoke a principal's access to a project.

list_roles

Read

List the roles this instance defines, with their ids.

The three membership write tools require OPENPROJECT_MCP_ADMIN_TOOLS=1 (and an OpenProject account with the Manage members permission).

Instance metadata and schemas (metadata)

Tool

Kind

What it does

get_instance_info

Read

Check the OpenProject connection and report what this instance supports.

get_project_metadata

Read

List the ids and names (types, statuses, priorities, ...) that are actually valid on this instance.

get_work_package_schema

Read

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

list_permissions

Read

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

enable_tool_group

Read

Re-enable a tool group hidden by OPENPROJECT_MCP_PROFILE=core for this session, bearer token or stdio process; never calls OpenProject.

Meetings (meetings)

The six recurring-meeting tools also carry the meetings_recurring tag, so OPENPROJECT_MCP_DISABLE=meetings_recurring drops just them (they need OpenProject 17.4+).

Tool

Kind

What it does

list_meetings

Read

List meetings: what is coming up, what already ran.

get_meeting

Read

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

create_meeting

Write

Schedule a meeting in a project and optionally invite participants.

update_meeting

Write

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

delete_meeting

Write (destructive)

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

add_meeting_agenda_item

Write

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

update_meeting_agenda_item

Write

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

delete_meeting_agenda_item

Write (destructive)

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

add_meeting_outcome

Write

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

update_meeting_outcome

Write

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

delete_meeting_outcome

Write (destructive)

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

list_recurring_meetings

Read

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

get_recurring_meeting

Read

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

create_recurring_meeting

Write

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

delete_recurring_meeting

Write (destructive)

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

init_recurring_meeting_occurrence

Write

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

cancel_recurring_meeting_occurrence

Write (destructive)

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

Wiki (wiki)

Tool

Kind

What it does

get_wiki_page

Read

Read a wiki page's identity and project — not its content (API v3 does not expose page bodies).

Documents (documents)

Tool

Kind

What it does

list_documents

Read

List the documents visible to you, across every project.

get_document

Read

Read one document with its full description text.

Budgets (budgets)

Tool

Kind

What it does

list_budgets

Read

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

News (news)

Tool

Kind

What it does

list_news

Read

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

get_news

Read

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

create_news

Write

Publish a news announcement in a project.

update_news

Write

Correct or rewrite a published news entry.

delete_news

Write (destructive)

Permanently delete a news entry.

Reporting (reporting)

Tool

Kind

What it does

get_project_report_data

Read

Aggregate a status report for one project and date window: exact counts, sampled rows with a drill-down call, and a state per section.

Prompts and resources

Report data uses closed_updated for currently closed work updated in the requested window. This replaces the former closed field in v0.3.0. It does not establish when work was completed; sprint health remains unassessed without supporting data.

Four prompt templates render live OpenProject data into ready-to-use briefings:

  • weekly_report — a weekly status report for one project: currently closed and updated / in progress / planned, hours and impediments.

  • daily_standup — today's standup for one project: yesterday's movement, what is due today, and what is blocked.

  • triage_inbox — groups your unread notifications by reason and suggests actions.

  • groom_backlog — sweeps a project's open backlog for unassigned, stale and unestimated work.

Three resource templates expose OpenProject objects at stable URIs:

  • openproject://work_package/{id} — one work package as JSON.

  • openproject://project/{identifier} — one project as JSON (identifier slug or numeric id).

  • openproject://attachment/{id} — the attachment's bytes with the detected MIME type, up to OPENPROJECT_MCP_MAX_INLINE_MB.

Clients that support argument completion get suggestions while typing: project identifiers for the project argument of weekly_report, daily_standup and groom_backlog and for the project resource, and the available locales for weekly_report.

Supported OpenProject versions

The server targets OpenProject 14 LTS through 17.x. Instead of assuming one API dialect, it probes the instance lazily on first need and caches the result for an hour. The version-dependent surfaces:

  • Target versions: work-package details expose target_versions on every supported instance. Create/update accept a list of version ids ([] clears); the schema selects targetVersions on 17.8+ or a single legacy version. The old version argument remains a single-value alias; do not pass both. On reads it is null for multiple assignments.

  • Internal (private) comments need OpenProject >= 16. Older servers silently ignore the internal flag, so below 16 the server refuses with a clear error rather than posting a comment publicly that you asked to keep internal.

  • Emoji reactions (toggle_comment_reaction) need OpenProject >= 16; detected from the version, tolerant of a 404 at call time.

  • Project favorites (set_project_favorite) need OpenProject >= 17; same tolerance.

  • Time-entry filters: the work-package filter is probed (entityId, falling back to the pre-15.x workPackage spelling).

  • Permission contexts (list_permissions): the context prefix is probed (p{id}, falling back to the w{id} spelling introduced in 17.2).

  • Meetings time filter: OpenProject 17.6 changed the wire dialect; list_meetings discovers which spelling the instance accepts and caches it.

  • Story points need the Backlogs module and a type Backlogs treats as a story. 17.x refuses story_points otherwise; 14.x accepts the write and silently drops the value, so the result carries a note saying it was not saved (bulk previews warn before writing).

Features that depend on optional instance modules (meetings, news, documents, budgets, wiki, backlogs, GitHub/GitLab integration, external storages) degrade honestly when the module is absent: list tools return an empty page with an in-band note naming the missing module, and detail tools return a structured error explaining both possible readings of the 404.

Security

  • The API key and OAuth token are held as SecretStr and never logged; Authorization and cookie headers are redacted from every log record. Request/response bodies are only logged at DEBUG level with OPENPROJECT_MCP_LOG_BODIES=1, for development.

  • The Authorization header is stripped whenever a redirect leaves the OpenProject origin — attachment downloads redirect to presigned object-storage URLs, and the credential must not travel there.

  • TLS is always verified. Private CAs are supported via OPENPROJECT_MCP_CA_BUNDLE; an insecure-TLS switch deliberately does not exist.

  • Attachment downloads respect OpenProject's virus scanner: quarantined files are never fetched and produce a structured attachment_quarantined error. Downloads are capped by OPENPROJECT_MCP_MAX_DOWNLOAD_MB, stored under sanitized file names (directory separators and traversal sequences are neutralized), and reported with a SHA-256 of the bytes.

  • Destructive tools require confirm=true and are annotated so clients can ask the user first.

  • The HTTP transport refuses to start without OPENPROJECT_MCP_AUTH_TOKENS configured and verifies the Authorization: Bearer header on every request — missing or invalid tokens are rejected with a 401, and token comparison is constant-time. The server does not terminate TLS: keep the default 127.0.0.1 bind, and put a TLS-terminating reverse proxy in front if the port must be reachable from anywhere else (see Transports). All HTTP clients share the server's one OpenProject identity; confine the file tools with OPENPROJECT_MCP_UPLOAD_ROOT / OPENPROJECT_MCP_DOWNLOAD_ROOT.

  • Downloads are created exclusively (never overwriting an existing file, even under concurrent downloads of the same name), and a failed or cancelled transfer removes only the file it created.

Troubleshooting

Symptom

What it means and what to do

Server exits with "cannot start, configuration is incomplete"

Run openproject-mcp-server --check and fix the problems it lists; the variable reference is the Configuration section above.

401 authentication_failed

The API key is wrong, revoked, or belongs to a blocked account. Generate a fresh token under My account → Access tokens.

403 permission_denied

The account is authenticated but lacks a role permission for that project, or the relevant module is disabled for it.

Empty list plus a note about a missing module

That OpenProject module (meetings, news, documents, ...) is not installed or not enabled in the project — the empty result is the honest answer, not an error.

TLS certificate errors

Your instance uses a private CA: point OPENPROJECT_MCP_CA_BUNDLE at its PEM bundle. There is no option to disable verification.

--transport http refuses to start

Set OPENPROJECT_MCP_AUTH_TOKENS (comma-separated bearer tokens), or OPENPROJECT_MCP_INSECURE=1 for local development only — see Transports.

401 from the MCP HTTP endpoint itself

The request's Authorization: Bearer <token> header is missing or does not match any configured OPENPROJECT_MCP_AUTH_TOKENS entry. Check the client's token for typos; distinct from the upstream 401 authentication_failed above, which is about the OpenProject API key.

Proxies

Standard HTTPS_PROXY / ALL_PROXY / NO_PROXY variables are honored by the underlying HTTP client.

When reporting an issue, please include your OpenProject version, the output of openproject-mcp-server --version, and the output of openproject-mcp-server --check.

Development

git clone https://github.com/kar-thik/openproject-mcp
cd openproject-mcp
uv sync --group dev
uv run pytest              # entire suite runs offline (respx-mocked HTTP), no instance needed
uv run ruff check .
uv run ruff format --check .
uv run pyright             # strict mode

The default suite stays offline. Opt-in compatibility checks run against fresh official OpenProject 14.6, 15.5, 16.6 and 17.8 containers in the Compatibility workflow (including both single- and multiple-version modes on 17.8). To run one locally with Docker running:

uv run python scripts/live_smoke.py --image openproject/openproject:17.8.0

The harness seeds a private project, temporary users, custom fields and versions; it tests through MCP, then removes the container and its volumes. Existing instances and credentials are not used. Direct pytest runs skip these tests unless --live-openproject is supplied with a disposable fixture file. The full technical specification lives in SPEC.md.

Releasing is documented in CONTRIBUTING.md.

License and trademark

MIT — see LICENSE.

This is a community project. It is not affiliated with, endorsed by, or supported by OpenProject GmbH. "OpenProject" is a trademark of OpenProject GmbH and is used here only to indicate interoperability.

Available Tools

59 tools
add_work_package_commentAdd 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.

Inputs: id (required), comment (required), notify, internal. Returns: id, kind, author, comment, +8 more

add_work_package_watcherAdd work package watcherA
Idempotent

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.

Inputs: user_id (required), work_package_id (required). Returns: user, changed, message, watching, +1 more

bulk_update_work_packagesPreview or apply work package updatesA

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

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

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

Inputs: updates (required), notify, dry_run. Returns: items, notes, total, applied, +2 more

copy_projectCopy 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.

Inputs: new_name (required), id_or_identifier (required), notify, include_work_packages. Returns: notes, job_id, source, status, +4 more

create_projectCreate projectA

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

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

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

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

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

Inputs: name (required), public, parent_id, identifier, +2 more. Returns: id, name, active, parent, +8 more

create_versionCreate 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.

Inputs: name (required), project_id (required), status, sharing, +3 more. Returns: id, name, source, status, +7 more

create_work_packageCreate work packageA

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

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

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

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

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

Inputs: type (required), project (required), subject (required), date, +17 more. Returns: id, date, type, notes, +27 more

create_work_package_relationCreate 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=...).

Inputs: type (required), to_id (required), from_id (required), lag, +1 more. Returns: id, lag, type, description, +3 more

delete_attachmentDelete attachmentA
Destructive

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.

Inputs: attachment_id (required), confirm. Returns: id, deleted, message, container, +1 more

delete_projectDelete projectA
Destructive

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.

Inputs: id_or_identifier (required), confirm. Returns: id, job_id, message, scheduled

delete_time_entryDelete time entryA
Destructive

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.

Inputs: time_entry_id (required), confirm. Returns: id, deleted, message

delete_versionDelete versionA
Destructive

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.

Inputs: version_id (required), confirm. Returns: id, deleted, message

delete_work_packageDelete work packageA
Destructive

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.

Inputs: id (required), confirm. Returns: id, deleted, message

delete_work_package_relationDelete work package relationA
Destructive

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.

Inputs: relation_id (required), confirm. Returns: id, deleted, message

download_attachmentDownload attachmentA
Read-only

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.

Inputs: attachment_id (required), save_dir, return_image. Returns: path, notes, sha256, file_name, +2 more

edit_work_package_commentEdit work package commentA
Idempotent

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.

Inputs: comment (required), activity_id (required). Returns: id, kind, author, comment, +8 more

enable_tool_groupEnable tool groupA
Read-onlyIdempotent

Re-enable a tool group hidden by OPENPROJECT_MCP_PROFILE=core.

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

Returns {group, title, status, tools_enabled, notes, scope}; status is 'enabled' or 'already_visible'. Re-read your tool list before calling the new tools.

Pitfalls: it only lifts the core profile. It cannot override read-only mode (write tools stay hidden, see notes), the admin gate, or groups the operator removed with OPENPROJECT_MCP_DISABLE, which fail with invalid_input. scope says who else sees the group: this session, everyone on the same bearer token, or this stdio process.

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

Inputs: group (required). Returns: group, notes, scope, title, +2 more

execute_custom_actionExecute 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.

Inputs: work_package_id (required), custom_action_id (required), lock_version. Returns: id, type, status, project, +8 more

get_instance_infoGet instance infoA
Read-onlyIdempotent

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.

Inputs: no named parameters. Returns: api_url, features, core_version, current_user, +3 more

get_job_statusGet background job statusA
Read-onlyIdempotent

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.

Inputs: job_id (required). Returns: id, notes, status, message, +4 more

get_projectGet projectA
Read-onlyIdempotent

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=...).

Inputs: id_or_identifier (required). Returns: id, name, active, parent, +8 more

get_project_metadataGet project metadataA
Read-onlyIdempotent

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.

Inputs: refresh, project_id. Returns: notes, roles, types, statuses, +5 more

get_project_phaseGet project phaseA
Read-onlyIdempotent

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.

Inputs: phase_id (required). Returns: id, name, notes, active, +4 more

get_sprintGet sprintA
Read-onlyIdempotent

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

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

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

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

Inputs: sprint_id (required). Returns: id, name, status, workspace, +5 more

get_userGet user detailA
Read-onlyIdempotent

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.

Inputs: id_or_me (required). Returns: id, name, admin, email, +5 more

get_work_packageGet work package detailA
Read-onlyIdempotent

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.

Inputs: id (required), include. Returns: id, date, type, notes, +32 more

get_work_package_schemaGet work package schemaA
Read-onlyIdempotent

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.

Inputs: type_id (required), project_id (required), refresh. Returns: notes, fields, type_id, project_id, +2 more

list_attachmentsList attachmentsA
Read-onlyIdempotent

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.

Inputs: container_id (required), container_type (required). Returns: sums, items, notes, groups, +1 more

list_membershipsList project membershipsA
Read-onlyIdempotent

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

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

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

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

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

Inputs: page, page_size, project_id, principal_id. Returns: sums, items, notes, groups, +1 more

list_notificationsList notificationsA
Read-onlyIdempotent

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

Inputs: page, reason, page_size, project_id, +1 more. Returns: sums, items, notes, groups, +1 more

list_permissionsList permissionsA
Read-onlyIdempotent

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.

Inputs: permission, project_id. Returns: sums, check, items, notes, +4 more

list_project_phase_definitionsList project phase definitionsA
Read-onlyIdempotent

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

Inputs: page, page_size. Returns: sums, items, notes, groups, +1 more

list_projectsList projectsA
Read-onlyIdempotent

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=...).

Inputs: page, active, search, sort_by, +6 more. Returns: sums, items, notes, groups, +1 more

list_queriesList saved queriesA
Read-onlyIdempotent

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.

Inputs: page, page_size, project_id. Returns: sums, items, notes, groups, +1 more

list_remindersList remindersA
Read-onlyIdempotent

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.

Inputs: no named parameters. Returns: sums, items, notes, groups, +1 more

list_rolesList rolesA
Read-onlyIdempotent

List the roles this instance defines, with their ids.

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

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

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

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

Inputs: include_permissions. Returns: sums, items, notes, groups, +1 more

list_sprintsList sprintsA
Read-onlyIdempotent

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

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

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

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

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

Inputs: page, page_size, project_id. Returns: sums, items, notes, groups, +1 more

list_time_entriesList time entriesA
Read-onlyIdempotent

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

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

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

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

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

Inputs: page, user, to_date, fetch_all, +6 more. Returns: sums, items, notes, groups, +1 more

list_versionsList versionsA
Read-onlyIdempotent

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

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

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

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

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

Inputs: project_id, include_sprints. Returns: sums, items, notes, groups, +1 more

list_work_package_commentsList work package commentsA
Read-onlyIdempotent

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.

Inputs: id (required), page, page_size, activity_id, +1 more. Returns: sums, items, notes, groups, +1 more

list_work_packagesList work packagesA
Read-onlyIdempotent

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

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

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

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

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

Inputs: page, query, author, project, +29 more. Returns: sums, items, notes, groups, +1 more

log_timeLog 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.

Inputs: hours (required), spent_on (required), comment, activity, +2 more. Returns: id, user, hours, comment, +7 more

mark_all_notifications_readMark all notifications readA
Idempotent

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.

Inputs: reason, project_id. Returns: ids, read, marked, message

mark_notificationsMark notifications read or unreadA
Idempotent

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.

Inputs: ids (required), read. Returns: ids, read, marked, message

remove_work_package_watcherRemove work package watcherA
Idempotent

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.

Inputs: user_id (required), work_package_id (required). Returns: user, changed, message, watching, +1 more

run_queryRun a saved queryA
Read-onlyIdempotent

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.

Inputs: query_id (required), page, page_size, override_filters. Returns: sums, items, notes, query, +2 more

save_querySave a queryA

Save a filter set as a reusable OpenProject view.

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

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

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

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

Inputs: name (required), filters (required), star, public, +3 more. Returns: id, name, notes, public, +7 more

search_principalsSearch users, groups and placeholder usersA
Read-onlyIdempotent

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

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

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

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

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

Inputs: page, type, query, status, +2 more. Returns: sums, items, notes, groups, +1 more

search_work_packagesSearch work packagesA
Read-onlyIdempotent

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.

Inputs: query (required), mode, page, page_size, +2 more. Returns: sums, items, notes, groups, +1 more

set_project_favoriteSet project favoriteA
Idempotent

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.

Inputs: favorite (required), id_or_identifier (required). Returns: id, message, favorite

set_work_package_reminderSet work package reminderA
Idempotent

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.

Inputs: work_package_id (required), note, remind_at. Returns: action, message, reminder, work_package_id

toggle_comment_reactionToggle 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.

Inputs: reaction (required), activity_id (required). Returns: notes, message, reacted, reaction, +2 more

update_projectUpdate 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.

Inputs: id_or_identifier (required), name, active, public, +4 more. Returns: id, name, active, parent, +8 more

update_time_entryUpdate 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.

Inputs: time_entry_id (required), hours, comment, activity, +1 more. Returns: id, user, hours, comment, +7 more

update_versionUpdate 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.

Inputs: version_id (required), name, status, sharing, +3 more. Returns: id, name, source, status, +7 more

update_work_packageUpdate work packageA

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

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

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

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

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

Inputs: id (required), date, type, notify, +18 more. Returns: id, date, type, notes, +27 more

update_work_package_relationUpdate work package relationA
Idempotent

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.

Inputs: relation_id (required), lag, type, description. Returns: id, lag, type, description, +3 more

upload_attachmentUpload 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.

Inputs: file_path (required), container_id (required), container_type (required), file_name, +1 more. Returns: id, author, status, file_name, +4 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 59 tools

    Disambiguation4/5

    Most tools have clearly distinct resource/action targets, and the descriptions explicitly separate commonly confused pairs such as list_attachments vs list_file_links and list_work_packages vs search_work_packages. The large set still has several adjacent read paths (list/search/run_query/get for work packages, versions vs sprints, metadata vs schema) that an agent must track carefully, but the boundaries are generally documented.

    Naming Consistency5/5

    Names consistently use snake_case with a verb_noun pattern across CRUD, bulk, comment, watcher, relation, notification, and meta tools. Compound resources such as work_package, time_entry, and project_phase are applied predictably, with no camelCase or mixed-verb chaos.

    Tool Count2/5

    At 59 tools, the surface is far beyond the typical 3-15 range and is heavy for an agent to navigate, even though OpenProject is a broad domain. The tools are mostly meaningful and non-redundant, but the set would benefit from stronger grouping or consolidation.

    Completeness4/5

    Core workflows are well covered: project CRUD/copy, work-package CRUD/bulk/comments/watchers/relations, attachments, time entries, versions, sprints, notifications, reminders, queries, phases, and permissions. Remaining gaps are mostly intentional or gated (wiki/meetings/documents/budgets hidden behind enable_tool_group, saved-view edit/delete deferred to UI, membership writes admin-gated).

    Maintenance

    ActivityActive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers