Skip to main content
Glama

openproject-mcp

An MCP server over the OpenProject APIv3 — work packages, comments, time entries, documents, users, and activities. Zero dependencies, stdio transport, one file.

Why this exists

OpenProject ships its own MCP server as of 17.2, but it is an Enterprise add-on (Professional, Premium, Corporate) and is read-only: "Right now OpenProject only offers read-only tools." That rules it out for the job this server does, which is creating and updating work packages.

The community alternatives were surveyed before writing this:

Server

Stack

Stars

Comments

Time entries

Blocker

AndyEverything

Python + uv

72

"Do not use it productively"; 5 delete tools

brunofin

TypeScript

1

4 commits total

Tangaratta

Python + FastMCP

14

SSE-only, fixed port

None combined comments with time entries, and all three carry immaturity warnings. Hence 18 tools, tightly scoped, no delete tools at all.

Related MCP server: OpenProject MCP Server

Tools

Readop_list_projects, op_list_work_packages, op_get_work_package, op_list_types, op_list_statuses, op_list_priorities, op_list_time_entry_activities, op_list_time_entries, op_list_users, op_list_activities, op_list_documents, op_get_document

Writeop_create_work_package, op_update_work_package, op_comment_work_package, op_log_time, op_update_document, op_add_document_attachment

Lists stay slim — id, subject, status, priority, and the like. op_get_work_package is the one that is genuinely full: description, custom fields, version, category, responsible, the spentTime/estimatedTime/remainingTime aggregates, and createdAt.

Every writable field is writable here

Create and update both take the whole schema-writable set, not a subset: priority, category and version by name; estimatedTime (Work) and remainingTime (Remaining work) as decimal hours, an ISO duration, or null to clear; percentageDone; assignee and responsible (Accountable); and type, which is settable on update, not only at creation. Names are resolved against the instance, and an unknown one fails with the valid values listed — that error is the discovery path, which is why there is no op_list_categories or op_list_versions.

Work, Remaining work and % Complete are one quantity in three views: OpenProject derives the third from the other two, so passing all three at once is refused up front rather than coming back as a 422. Pass at most two.

op_list_projects and op_list_work_packages page via pageSize (capped at 200) and offset, returning total/offset/limit; op_list_time_entries takes pageSize too. On op_list_work_packages, project: "all" searches across every project even when OPENPROJECT_PROJECT_ID is set.

op_list_users resolves assignable principals, and it does not need /api/v3/users — that endpoint answers 403 for an ordinary API key, which used to make assignment unusable. It falls back outwards from the project's available_assignees (which needs only project membership) to /principals to /users, and reports in source which list answered. Because of that, assignee and responsible accept a display name or login as well as an id; a name that matches nothing or matches more than one principal fails with the candidates listed. Groups and placeholder users resolve to their own collections, so assigning a group works.

op_list_activities reads comments back, the read-your-comments counterpart to op_comment_work_package.

Documents

op_list_documents lists the Documents-module documents, newest first, scoped to the default project (or project: "all"); it filters by search (title) and sorts by sortBy (created_at, updated_at, id) with order. op_get_document is the genuinely full one: title, markdown description, project, dates, and the document's attachments (file name, size, MIME type, upload status, author, download URL).

APIv3 has no document create or deletePOST /api/v3/documents answers 404 on every version, and the official docs call the endpoint "only a stub". Documents are created in the UI; the MCP side covers the rest of the lifecycle: op_update_document (title and/or markdown description — there is no lockVersion on documents, so concurrent edits are last-write-wins) and op_add_document_attachment, which uploads base64 file bytes as multipart/form-data. Uploads are capped at the instance's own maximumAttachmentFileSize from /api/v3/configuration (5 MiB fallback). Both tools need the manage_documents permission; reading needs view_documents.

The document API has two quirks the server absorbs. The description {"raw": …} form documented for every other resource corrupts document text on documents — the stored value becomes Ruby's '{raw: "…"}' literal, and on 17.3.1 ASCII-only bodies even 500 the response (verified live; the update contract is unchanged through 17.7, whose OpenAPI spec still documents the same broken form) — so the server sends the plain string the UI itself sends, which stores cleanly on every version. And filters on the collection take numeric project ids only (never identifiers), which is why op_list_documents' project argument is resolved internally, exactly like the time_entries filter.

Writes default to notify=falseop_log_time doesn't even expose the flag — so bulk agent activity does not email the whole project. op_update_work_package fetches lockVersion itself, so a concurrent edit fails loudly instead of silently overwriting; parent: null on an update clears the parent link. Arguments are validated up front: ids must be positive integers, dates must be YYYY-MM-DD, and unknown statuses/activities/ids fail with messages that name the offending value.

Configuration

Resolution order:

  1. OPENPROJECT_ENV_FILE — path to a .env to read OPENPROJECT_* from. Values in an explicitly named .env win over the ambient environment. Deliberate: a stale exported OPENPROJECT_API_KEY silently shadowing a freshly rotated one in .env cost hours of debugging — the API reports it as 401 You did not provide the correct credentials, indistinguishable from a bad key. Naming a file is a deliberate act; an inherited variable usually is not.

  2. OPENPROJECT_URL (or OPENPROJECT_BASE_URL) + OPENPROJECT_API_KEY + OPENPROJECT_PROJECT_ID from the environment

  3. ~/.config/openproject-mcp/config.json{"url": "...", "apiKey": "...", "defaultProject": "..."} (consulted only when a URL or key is still missing)

OPENPROJECT_PROJECT_ID sets the default project, so most calls need no project argument.

OPENPROJECT_HOURS_DB (optional) points the hours-ledger tap at a SQLite cache; it defaults to ~/Projects/hours/hours.db. An empty value disables the tap.

The hours-ledger tap

This machine runs a second, unrelated tool — the hours tracker at ~/Projects/hours — that keeps the team sheet's side of the time ledger in SQLite (entry rows keyed by OpenProject work package id, statuses draft/approved/pushed, plus a cached task row per package). It also writes OpenProject time entries of its own when a tracked entry carries a task id, which makes it a second writer alongside op_log_time.

So this server peeks at that cache — read-only, always best-effort — and reports the local side where it matters:

  • op_get_work_package returns hoursLedger next to spentTime: the sheet's own minutes for that task, split into draft/approved/pushed, plus the cached task subject/status when known.

  • op_log_time returns hoursLedger alongside the created entry, so the sheet's side of the ledger is visible at the moment of the write.

hoursLedger is present only when the cache is installed and readable; absent otherwise. The two ledgers describe the same work, so they are never summedspentTime and sheet.totalMinutes must be read as alternatives, not added. Nothing here blocks or dedupes a write; the tap exists to inform the caller, and the server is fully functional with the hours tool absent, disabled (OPENPROJECT_HOURS_DB=""), or unreachable. The cache is SQLite in WAL mode, and the tap opens it read-only with short queries, so it is safe to run while the hours CLI, MCP server, or collector daemon are writing. Requires Node ≥ 22.5 (built-in node:sqlite); on older Node the tap stays off and nothing else changes.

When both MCP servers are registered in the same client, the hours server's task_hours {taskId, refresh} tool is the fuller union view (OpenProject's spentTime plus the local ledger in one call), and op_list_time_entries verifies time entries the hours push write-through created.

Auth is HTTP Basic with the literal username apikey and the key as password. The key is never written to stdout, stderr, or any tool result — and the URL must be https (plain http is rejected unless the host is localhost or 127.0.0.1), because the key must not travel in the clear.

OPENPROJECT_TIMEOUT_MS sets the per-request timeout (default 30000). Transient failures (429, 502, 503, 504) are retried twice with backoff, honoring Retry-After — GETs only; writes are never retried, so a timed-out create is not silently replayed.

.env and config.json should be chmod 600 — the server warns on stderr when either is readable by others. An unreadable or missing OPENPROJECT_ENV_FILE prints a stderr warning and falls through to the remaining sources instead of failing silently.

Install

Requires Node.js ≥ 18. The server is a single self-contained file with no dependencies — no build step, nothing to compile. Install it in one line, then register it with your client below.

macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/DDeluca06/openproject-mcp/master/scripts/install.sh | bash

Windows (PowerShell):

irm https://raw.githubusercontent.com/DDeluca06/openproject-mcp/master/scripts/install.ps1 | iex

Both download server.mjs into ~/.local/bin (Windows adds an openproject-mcp.cmd shim, since shebangs don't work there) and print the next steps. If ~/.local/bin isn't on your PATH (macOS and Windows by default), the installer tells you how to add it:

export PATH="$HOME/.local/bin:$PATH"   # put this line in ~/.zshrc or ~/.bashrc

GUI-launched apps (VS Code, some terminals) inherit a minimal PATH — if your client can't find openproject-mcp, use the absolute path the installer printed.

Alternative (npm, all three platforms) — installs the same one file with a proper bin entry:

npm install -g --allow-remote https://github.com/DDeluca06/openproject-mcp/tarball/master

--allow-remote is required on npm ≥ 12, which blocks GitHub tarballs by default; older npm versions ignore the flag.

From a clone (development, or if you'd rather not pipe a script into a shell):

git clone https://github.com/DDeluca06/openproject-mcp.git
cd openproject-mcp
cp .env.example .env && chmod 600 .env   # then paste your API key into .env
node server.mjs                          # or: npm start

There is nothing to install — npm install fetches no dependencies. Point your client at the absolute path to server.mjs instead of the openproject-mcp command.

Either way, verify the server starts: openproject-mcp should run and wait on stdin.

Claude Code

claude mcp add openproject -s user \
  -e OPENPROJECT_URL=https://projects.example.com \
  -e OPENPROJECT_PROJECT_ID=your-project-identifier \
  -e OPENPROJECT_ENV_FILE=/path/to/.env \
  -- openproject-mcp

The snippet sets both OPENPROJECT_URL and OPENPROJECT_ENV_FILE; if the .env also holds OPENPROJECT_URL, the -e value is ignored — deliberate, so a freshly rotated .env key wins.

OpenCode

In ~/.config/opencode/opencode.json, under the top-level mcp key — not nested under mcp.servers, which fails schema validation and silently disables every server in the file:

{
  "mcp": {
    "openproject": {
      "type": "local",
      "command": ["openproject-mcp"],
      "enabled": true,
      "environment": {
        "OPENPROJECT_URL": "https://projects.example.com",
        "OPENPROJECT_PROJECT_ID": "your-project-identifier",
        "OPENPROJECT_ENV_FILE": "/path/to/.env"
      }
    }
  }
}

Verify with claude mcp get openproject and opencode mcp list.

Tests

npm test              # mock suite (no credentials needed) + live smoke test
npm test -- --writes  # smoke also creates a real work package, comment, and time entry

npm test runs test/mock.test.mjs (a deterministic fake APIv3 server, 114 checks, no credentials) and then test/smoke.mjs against the live instance; the smoke test self-skips when OPENPROJECT_URL is unset. --writes creates a real work package, comment, and time entry — the work package is clearly marked as a smoke test, then permanently deleted through the APIv3 DELETE endpoint after the run (its time entries go with it). Documents have no APIv3 create, so the write smoke instead renames an existing document to a SMOKE TEST … title, immediately reverts title and description, and uploads a tiny attachment that is then deleted through the raw API — if the revert or delete is interrupted, the remaining mark is a SMOKE TEST title or a smoke-test-*.txt attachment. The MCP tool surface itself still has no delete tool — the test cleanup bypasses it on purpose. If the API key lacks the delete permission the deletion is skipped loudly (DELETE 403), and the work package is left closed for manual cleanup; pass --keep to always leave the records in place.

APIv3 quirks this server absorbs

Each of these was found by a failing call, not from the docs:

  • A parentless work package still returns parent: {href: null} rather than omitting the link, so naive parent.href.split() throws.

  • time_entries rejects a project identifier and demands the numeric id, unlike the work_packages endpoints which take either. Resolved and cached internally.

  • The work-package filter on time_entries is entity, not work_package — the resource was generalised to attach to meetings too, and the old filter name no longer exists.

  • TimeEntriesActivity has no collection endpoint. The server first asks /api/v3/time_entries/schema for activity.allowedValues; instances that omit it fall back to probing /api/v3/time_entries/activities/{id} in parallel batches of 12, stopping after the first fully-missed batch (capped at id 120). The set is cached for 30 minutes. That plural /time_entries/activities/{id} path is an undocumented alias — it works, but it is not in the API docs.

  • hours is an ISO-8601 duration (PT1H30M; day components like P1DT2H parse too). op_log_time accepts decimal hours and converts; reads return both forms plus a decimal total. A duration that will not parse comes back as hoursDecimal: null with a parseWarnings count in op_list_time_entries — not silently as 0.

  • op_log_time's default spentOn is the local calendar date, not UTC — "today" in the server's timezone.

  • Time entries are always attributed to the API key's own user. OpenProject makes that field read-only, so time cannot be logged on someone else's behalf.

  • The Documents endpoint has no create (POST /api/v3/documents → 404) and no delete, and its own docs say "only a stub for now". PATCH writes title/description, attachments upload below.

  • A document's description must be PATCHed as a plain string, not the {"raw": …} hash (see the Documents section) — on 17.3 the hash form stores Ruby's inspected literal and the response can arrive as a 500 with text must be UTF-8 encoded even though the write committed.

  • Uploading a document attachment is multipart, but the metadata part must be a plain form-data string: sending it as a blob makes undici attach filename="blob", Rack treats the part as a file upload instead of JSON, and the API answers 500 no implicit conversion of ActiveSupport::HashWithIndifferentAccess into String.

  • The documents collection filter takes numeric project ids only — an identifier in filters=[{"project":…}] fails, resolved and cached internally like time_entries.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that connects Claude Desktop to your OpenProject instance, allowing you to manage projects, tasks, and time entries through natural language.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server enabling AI agents to interact with OpenProject API v3 for project management, including creating and managing work packages, projects, comments, time entries, boards, and user dashboards.
    42
    MIT

Latest Blog Posts

MCP directory API

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

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

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