Add CLI MCP server - #268
Conversation
…erver Gives coding agents a step-by-step procedure for adding taiga-mcp-server to a user's global (--scope user) Claude Code config: resolving a stable install (pip install --user by default, pipx/uvx as alternatives), collecting credentials safely, the exact claude mcp add invocations, and verification steps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stock Taiga has no self-service personal access token feature. The only tokens available are short-lived auth JWTs (24h/8-day on the instance checked) and admin-gated OAuth Application tokens, and this server's CLI has no refresh-token support. Default agent guidance to username/password and only suggest TAIGA_TOKEN when the target instance is verified to offer a durable personal token. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds docs/mcp.rst covering what the MCP server is, installing the mcp extra (pip install --user / pipx / uvx), the TAIGA_HOST/TAIGA_TOKEN/ TAIGA_USERNAME/TAIGA_PASSWORD/TAIGA_TLS_VERIFY configuration (env vars and equivalent CLI flags), running it standalone, registering it with an MCP client such as Claude Code, the full tool list grouped by entity, and a security note on write-tool blast radius. Wired into the toctree in docs/index.rst. Verified with a clean -W sphinx-build. Also picks up an unrelated AGENTS.md edit (anonymizing the example Taiga host) that was already pending in the working tree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Restore the testenv:docs section (dropped from tox.ini in 923cabc while "docs" stayed in envlist), and add setuptools to its deps so invoke's clean pre-task (python setup.py clean --all) works. - Fix MANIFEST.in: include AGENTS.md and correct the requirements-tests.txt typo to requirements-test.txt, fixing check-manifest failures in the pypi-description env. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3d50687 to
582c540
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #268 +/- ##
==========================================
+ Coverage 96.47% 96.93% +0.45%
==========================================
Files 8 12 +4
Lines 936 1271 +335
Branches 67 90 +23
==========================================
+ Hits 903 1232 +329
- Misses 19 22 +3
- Partials 14 17 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
582c540 to
c314fee
Compare
Install the mcp extra in requirements.txt so fastmcp is available wherever the test suite runs (tox py311-py314 were failing to collect tests/test_mcp_server.py with ModuleNotFoundError: No module named 'fastmcp'). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
c314fee to
578d25b
Compare
Replace the third-party fastmcp dependency with mcp.server.mcpserver.MCPServer from the official MCP Python SDK. mcp 2.0 renamed FastMCP to MCPServer (no back-compat alias) and requires the @mcp.tool() call form instead of the bare @mcp.tool decorator. No behavior change: tool signatures, docstrings, and CLI usage are unchanged. Verified against a real mcp~=2.0 install (66 mcp_server tests + a manual stdio smoke test) and via `tox -e py311 -r` (289 tests passing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The .tox cache key is hashFiles('setup.cfg'), correctly busting the cache
whenever dependencies change. But the restore-keys fallback (unhashed prefix)
undermines that: on a cache-key miss it restores the most recent .tox env
built against an older setup.cfg, and a plain `tox -e<env>` run won't
re-resolve dependencies against the new one (tox only reinstalls deps when
their own declaration text changes, not when setup.cfg's extras do) - so CI
would run tests against stale, possibly-incompatible dependencies.
Reproduced locally: this exact mechanism left .tox/py312-314 with the
pre-swap mcp==1.29.0 after the fastmcp -> mcp~=2.0 change in setup.cfg,
causing a ModuleNotFoundError for mcp.server.mcpserver. Fixed locally with
`tox -e <env> -r`; this commit removes the same trap from CI by dropping
the restore-keys fallback for the .tox cache in both workflows. The pip
cache's restore-keys are left as-is - that one is just a download cache,
safe to partially warm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User-provided numbers (e.g. extracted from a Taiga URL like
.../issues/45634) are per-project refs, not database ids. Make
get_/update_/delete_{user_story,task,issue,epic} and add_comment take
project+ref as the primary lookup, resolving through Project's
get_*_by_ref() endpoints. get_history resolves ref->id the same way
for those four entity types; wiki pages have no ref in Taiga, so
entity_type="wiki" keeps taking a literal id and no project.
Add *_by_id counterparts (get_issue_by_id, update_task_by_id,
delete_epic_by_id, add_comment_by_id, get_history_by_id, ...) as a
secondary, documented-as-non-default path for callers that already
hold the raw database id. Milestones and wiki pages are unchanged -
Taiga has no ref concept for either.
Rewrote tests/test_mcp_server.py for every changed and added tool.
Documented the ref/id distinction and primary/secondary tools in
docs/mcp.rst.
There was a problem hiding this comment.
Pull request overview
Adds an optional stdio MCP server exposing python-taiga functionality to AI agents.
Changes:
- Adds authenticated MCP tools for reading and managing Taiga entities.
- Adds CLI, packaging, documentation, and registration guidance.
- Adds tests and updates CI/tox configuration.
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/lint.yml |
Prevents stale tox cache restoration. |
.github/workflows/test.yml |
Tightens tox cache keys. |
.gitignore |
Ignores local development artifacts. |
AGENTS.md |
Documents global MCP registration. |
MANIFEST.in |
Updates source-distribution contents. |
artifacts/activity-log.md |
Records the SDK migration. |
artifacts/evaluations/2026-08-24-mcp-sdk-rewrite.md |
Captures migration evaluation. |
changes/267.feature |
Adds the release-note fragment. |
docs/index.rst |
Includes MCP documentation. |
docs/mcp.rst |
Documents installation, tools, and security. |
requirements.txt |
Installs the MCP extra for development. |
setup.cfg |
Packages submodules and registers the CLI. |
taiga/mcp_server/__init__.py |
Defines the MCP package. |
taiga/mcp_server/auth.py |
Builds and caches authenticated clients. |
taiga/mcp_server/cli.py |
Implements the stdio server command. |
taiga/mcp_server/serialize.py |
Serializes Taiga resources. |
taiga/mcp_server/server.py |
Implements MCP tools. |
tests/test_mcp_server.py |
Tests tool routing and behavior. |
tests/test_mcp_server_auth.py |
Tests authentication configuration. |
tests/test_mcp_server_cli.py |
Tests CLI and environment handling. |
tox.ini |
Restores docs testing and manifest exclusions. |
Suppressed comments (9)
taiga/mcp_server/server.py:161
projecthas no default, so MCP's generated input schema requires clients to send it even for wiki history, contradicting the documented instruction to omit it. Reorder the parameters sorefremains required andprojectdefaults toNone, then update the positional tests accordingly.
def get_history(
entity_type: Literal["user_story", "task", "issue", "epic", "wiki"],
project: str | int | None,
ref: int,
) -> list[dict[str, Any]]:
taiga/mcp_server/server.py:244
- The real
InstanceResource.patch()returns the cached model with onlyversionchanged, unlike the test mock's updated dictionary, so this response contains stale field values. Fetch the updated user story before returning it.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:309
patch()does not update the model's changed attributes, so serializing its return value reports the task's old state after a successful update. Re-fetch by database ID before returning.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:316
- The underlying
patch()only updatesversionon this existing object; all other returned fields remain stale. Retrieve the task again after applying the patch.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:387
- Serializing
patch()'s return value reports the pre-update issue becauseInstanceResource.patch()does not hydrate response fields. Re-fetch the issue after patching so callers receive its current state.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:394
- The concrete
patch()implementation returns this cached resource and updates onlyversion, making the tool response stale for every requested field. Fetch the issue again before serializing it.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:455
InstanceResource.patch()does not hydrate the updated attributes ontoresource, so this response still contains the epic's old values. Re-fetch it by ID after the patch.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:462
- The returned model remains stale because the library's
patch()method only copiesversionfrom the response. Retrieve the updated epic before returning it.
return to_jsonable(resource.patch(list(fields.keys()), **fields))
taiga/mcp_server/server.py:150
- The client's
add_comment()returns the cached entity rather than the newly added comment, so this tool's payload is stale and misleading. Return a success acknowledgement or explicitly retrieve the new history entry.
return to_jsonable(resource.add_comment(comment))
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| query.setdefault("page", 1) | ||
| query.setdefault("page_size", DEFAULT_PAGE_SIZE) |
| def update_user_story(project: str | int, ref: int, fields: dict[str, Any]) -> dict[str, Any]: | ||
| """Update a user story identified by its per-project ref number. `fields` is a dict of the attributes to change.""" | ||
| resource = _get_by_ref("user_story", project, ref) | ||
| return to_jsonable(resource.patch(list(fields.keys()), **fields)) |
| def update_wiki_page(id: int, fields: dict[str, Any]) -> dict[str, Any]: # noqa: A002 | ||
| """Update a wiki page. `fields` is a dict of the attributes to change.""" | ||
| resource = get_client().wikipages.get(id) | ||
| return to_jsonable(resource.patch(list(fields.keys()), **fields)) |
| ) -> dict[str, Any]: | ||
| """Add a comment to a user story, task, issue or epic identified by its per-project ref number.""" | ||
| resource = _get_by_ref(entity_type, project, ref) | ||
| return to_jsonable(resource.add_comment(comment)) |
…nt state Address 4 review comments on PR #268 (commit cb66914): - _paginated(): filters was forwarded straight into ListResource.list(), so pagination=False (a client-control kwarg) or an explicit but falsy page/page_size (None, 0) bypassed the page-1/page_size-100 bound and could trigger an unbounded full-collection fetch. Strip `pagination` and normalize falsy page/page_size instead of dict.setdefault(). - update_*/update_*_by_id (9 call sites) and update_wiki_page: InstanceResource.patch() only refreshes `version` on the local object, not the fields the server actually applied - serializing the patched object directly returned stale pre-update values. Re-fetch the resource after patching before serializing it. - add_comment/add_comment_by_id: CommentableResource.add_comment() delegates to update(), which has the same staleness issue and never carries the comment itself (comments are history entries, not a resource field). Return an explicit {"status": "commented", ...} acknowledgement instead of serializing the stale resource. Updated tests/test_mcp_server.py for all of the above.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (5)
Previously missed (3) — in code that hasn't changed since the last review.
taiga/mcp_server/server.py:9
- The PR description still says this server and its extra use
fastmcp, while this import andsetup.cfguse the officialmcpSDK. Update the PR description so users and reviewers are not given the wrong dependency and architecture.
This issue also appears in the following locations of the same file:
- line 14
- line 172
from mcp.server.mcpserver import MCPServer
tests/test_mcp_server_cli.py:50
- This test expects
credentials.tokento beNone, butargparsedefaults it fromTAIGA_TOKEN; it therefore fails whenever the developer or CI environment already defines that variable. Clear the environment for this argv-only test.
cli.main(["--username", "alice", "--password", "secret", "--tls-verify"])
docs/mcp.rst:79
- Environment variables are not categorically hidden from other processes; on common systems they may be readable by same-user or privileged processes. Keep the recommendation, but avoid presenting environment variables as a secrecy boundary.
.. warning:: Prefer the environment variables over the CLI flags for
``--token``/``--password``: command-line arguments are visible
to other processes on the same machine (e.g. via ``ps``),
environment variables set for the server's own process are not.
taiga/mcp_server/server.py:172
- The documentation says wiki callers can omit
project, but this parameter has no default. The MCP SDK derives required fields from the Python signature, so a wiki call withoutprojectis rejected before reaching theentity_type == "wiki"branch. Make it genuinely optional (for example, reorder the parameters to allowproject=None) and update positional callers/tests accordingly.
project: str | int | None,
taiga/mcp_server/server.py:14
- The new tests invoke the Python functions directly and mock
mcp.run, but never exercise MCP tool discovery or generated schemas. A protocol-level smoke test fortools/list(including required/optional fields) and one tool call would catch registration and schema regressions that these unit tests cannot.
mcp = MCPServer(
Adds an MCP (Model Context Protocol) server to python-taiga, exposing Taiga entities as tools that AI agents (e.g. Claude Code) can call directly.
References
Fix #267
Checklist