Skip to content

Add CLI MCP server - #268

Open
yakky wants to merge 13 commits into
masterfrom
feature/issue-267-add-mcp
Open

Add CLI MCP server#268
yakky wants to merge 13 commits into
masterfrom
feature/issue-267-add-mcp

Conversation

@yakky

@yakky yakky commented Aug 23, 2026

Copy link
Copy Markdown
Member

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.

  • New taiga.mcp_server package built on fastmcp:
    • server.py — tools covering projects, user stories, tasks, issues, epics, milestones and wiki pages (list/get/create/update/delete as applicable), plus whoami and get_history (reads the full comment/change history of a user story, task, issue, epic or wiki page).
    • auth.py — client construction from TAIGA_HOST + TAIGA_TOKEN, or TAIGA_HOST + TAIGA_USERNAME/TAIGA_PASSWORD, with TAIGA_TLS_VERIFY support.
    • cli.py — standalone entry point with equivalent CLI flags for running the server directly.
    • serialize.py — helper to make Taiga model objects JSON-serializable for tool responses.
  • AGENTS.md — instructions for registering the Taiga MCP server with an agent, clarifying token vs. password auth.
  • docs/mcp.rst — new Sphinx page documenting installation (the mcp extra), configuration, running standalone, registering with an MCP client, the full tool list, and a security note on the blast radius of write tools. Wired into docs/index.rst.
  • tests/test_mcp_server.py — coverage for the new server.
  • Packaging: setup.cfg gains an mcp extra (fastmcp dependency); MANIFEST.in fixed to include AGENTS.md and correct a requirements-tests.txt → requirements-test.txt typo (was failing check-manifest in the pypi-description tox env).
  • tox.ini: restored the testenv:docs section (previously dropped while docs stayed in envlist) so all tox environments run cleanly.
  • changes/267.feature towncrier fragment added.

References

Fix #267

Checklist

  • I have read the contribution guide
  • Code lint checked via inv lint
  • changes file included
  • Usage documentation added (docs/mcp.rst, AGENTS.md)
  • Tests added

yakky and others added 5 commits August 23, 2026 19:10
…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>
@yakky yakky self-assigned this Aug 23, 2026
@yakky
yakky force-pushed the feature/issue-267-add-mcp branch from 3d50687 to 582c540 Compare August 23, 2026 17:29
@yakky
yakky marked this pull request as ready for review August 23, 2026 17:30
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.20896% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.93%. Comparing base (cbd94ee) to head (482fadf).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
taiga/mcp_server/serialize.py 64.70% 3 Missing and 3 partials ⚠️
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     
Flag Coverage Δ
unittests 96.93% <98.20%> (+0.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coveralls

coveralls commented Aug 23, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 98.269% (+0.3%) from 97.97% — feature/issue-267-add-mcp into master

@yakky
yakky force-pushed the feature/issue-267-add-mcp branch from 582c540 to c314fee Compare August 23, 2026 17:44
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>
@yakky
yakky force-pushed the feature/issue-267-add-mcp branch from c314fee to 578d25b Compare August 24, 2026 07:47
yakky and others added 6 commits August 24, 2026 16:34
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.
@yakky
yakky requested review from protoroto and a balanced review from Copilot August 31, 2026 10:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • project has 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 so ref remains required and project defaults to None, 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 only version changed, 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 updates version on 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 because InstanceResource.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 only version, 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 onto resource, 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 copies version from 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.

Comment thread taiga/mcp_server/server.py Outdated
Comment on lines +82 to +83
query.setdefault("page", 1)
query.setdefault("page_size", DEFAULT_PAGE_SIZE)
Comment thread taiga/mcp_server/server.py Outdated
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))
Comment thread taiga/mcp_server/server.py Outdated
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))
Comment thread taiga/mcp_server/server.py Outdated
) -> 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.
@yakky
yakky requested a balanced review from Copilot August 31, 2026 11:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and setup.cfg use the official mcp SDK. 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.token to be None, but argparse defaults it from TAIGA_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 without project is rejected before reaching the entity_type == "wiki" branch. Make it genuinely optional (for example, reorder the parameters to allow project=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 for tools/list (including required/optional fields) and one tool call would catch registration and schema regressions that these unit tests cannot.
mcp = MCPServer(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add MCP to allow agents interact with taiga API

3 participants