langchain-taiga
Taiga integration for LangChain and the Model Context Protocol.
The package ships three things in one install:
- 25 LangChain tools for Taiga (entities, wiki, custom attributes, members, sprint planning).
- A
TaigaToolkitthat bundles them for one-line LangChain agent setup. - An MCP server in two flavours:
- Stdio mode — single-user, local credentials in env vars. For Claude Desktop, Claude Code, VSCode local.
- Remote mode — multi-tenant HTTP server with OAuth 2.1 + PKCE + Dynamic Client Registration. For claude.ai Custom Connectors, VSCode Web, Claude Desktop with HTTP transport, etc. Each user signs in with their own Taiga credentials; the server stores no static API key.
The 27 tools:
create_entity_tool: Creates user stories, tasks, issues and epics in Taiga.statusis resolved for every entity type and an unknown one is a 404 — before 2.18.0 only issues did that: user stories dropped the status silently and landed in the project default, tasks raisedIndexErrorbehind a "Creation failed: list index out of range", and epics ignored it. Passmilestoneto file it straight into a sprint (the sprint name, orcurrent). Since 2.23.0custom_attributes(names or ids) are checked before the entity is created and set right after; if only that write fails the answer still sayscreatedand addscustom_attributes_error.search_entities_tool: Searches for user stories, tasks and issues in Taiga. Returns{matches, count, max_results, truncated}. Supportsmax_resultsandinclude_custom_attributes(defaultFalse— opt-in to avoid an N+1 fetch storm). Date filters are tz-aware. Each match reports bothowner(who filed it) andassigned_to(who is responsible now), and either can be filtered on — "issues created by jdoe" vs. "issues assigned to jdoe" — plusis_closed,milestoneandmilestone_name, so grouping by sprint or dropping finished work needs no per-match detail call. Passopen_only=Trueto exclude closed items: negation is not expressible in the query text, and phrasing it there ("not closed and not archived") only strikes the status names that literally appear, leaving siblings like "Done" or "Rejected" in the filter.fieldsandcompactcut the answer down (see Token-lean reads);count,max_resultsandtruncatedalways stay.get_kanban_board_tool: Returns the user-story Kanban board grouped into ordered status columns (mirrors the Taiga UI). Each column carriesstatus,status_id,order,is_closed,wip_limitandcards(sorted bykanban_order);include_closed=Falsehides closed columns and, since 2.19.0, asks Taiga for open stories only instead of listing the whole project first.statuses(exact names or ids) limits the board to those columns and is pushed down the same way. Withfields, cards can also carrymodified_date,created_date,due_date,tagsandis_blocked, and since 2.20.0tasks: a summary of every task of the story (id,ref,subject,status_id,is_closed,is_blocked,is_iocaine), fetched in the same request via Taiga'sinclude_tasks=1. Three more card keys cost one request per card and come only when a path names them:custom_attributes(values by attribute id), andlast_activity_at/last_comment_atfor the users inactivity_users(usernames, ids orme). The server reads them in parallel, and a card that still fails after one retry fails the whole call rather than coming back empty. Since 2.23.0tasks.custom_attributes(one request per task, named-only, read the same way) and, whenever acustom_attributespath is asked for,custom_attribute_names(attribute names by id per kind). User stories only.get_kanban_boards_tool(2.23.0): Several boards in one call —project_slugsorslug_prefix, the samefields/statuses/include_closed/activity_usersfor every board, read one after another, at most 20. A board that fails is an entry witherrorandcodeand is listed infailed, so it never reads as an empty board.get_entity_by_ref_tool: Gets a user story, task or issue by reference. The response includesowner({id, username, full_name}— the creator, which Taiga tracks separately fromassigned_toand which no longer needs a history walk to recover). For user stories, the response also includes apointsfield ({role_name: value}shape, symmetric toset_userstory_points_tool's input) so a read + write round-trip stays in the same vocabulary.tagsis returned as flat names (not Taiga's[name, color]wire shape) for the same reason — it is exactly whatmanage_tags_by_ref_tooltakes as input. Passinclude_history=Falsewhen only the current state is needed: history is 84–97% of this payload on real tickets (up to ~190 KB for one busy story, which drops to ~4 KB), and skipping it also saves an API round-trip. Thehistorykey is then omitted rather than empty, because an empty history is a real answer — Taiga writes no entry for creation, so a never-edited ticket genuinely has none. Since 2.19.0,fieldsfetches only what the listed paths need (history, tasks, custom attributes and user lookups are skipped otherwise), andhistory_since/history_user(me, a username or an id) /history_comments_only/history_limitfilter the history rows, withhistory_totalandhistory_returnedsaying how much was left out. Since 2.22.0related.tasks.historyembeds each task's history, filtered like the story's; it needshistory_sinceand reads only the tasks modified since then (one call for a 306-task story: 63 KB, 3.9 s). Since 2.20.0 afieldspath can also ask forstatus_id, a user story'sassigned_users({id, username, full_name}each) andrelated.tasks.id/status_id/modified_date; none of them appear in the default answer.update_entities_by_ref_tool(2.22.0): Applies one change (status, assignee, watchers, tags, comment) to up to 100 entities of one type in a single call — each gets exactly whatupdate_entity_by_ref_tool(strict=True)would do for it: its own PATCH, its ownexpected_versionand its own answer (withtarget, andstatewhenread_backis on, the default). An item can bring its own comment. One entity failing does not stop the others;succeededandfailedlist the refs. Built for hand-overs of many tasks at once.update_entity_by_ref_tool: Updates a user story, task or issue by reference. Since 2.18.0 this includesmilestone— the sprint name, orcurrentfor the sprint covering today, or an empty string to take it out of its sprint. Sprint membership previously had no MCP path at all and needed a hand-rolled RESTPATCH. User stories and issues only: a task's sprint follows its user story, and epics have none. Since 2.19.0 it also takescomment,watchers(+watchers_mode) andtags(+tags_mode) and sends everything in one PATCH, so a hand-over leaves one history entry.strict=Trueresolves status and assignee only exactly (never through the language model, which otherwise matches "finished" to "Done"), andread_back=Truereturns what Taiga stored (state, the newesthistory_entry, andcomment_entries). Since 2.20.0assigned_userssets a user story's complete assignee list in the same PATCH, always resolved exactly, andread_backreports it;state.idscarries the stored status, assignee, watchers and assignees as ids, which is what a scripted write compares against. Since 2.21.0expected_versionties a write to the version the caller read (get_entity_by_ref_toolreturns it asversionon request): if the entity has changed since, nothing is written and the answer is a 409 withcurrent_version. A new-style answer also names itstarget(project, ref, type).manage_watchers_by_ref_tool: Adds, replaces, or removes watchers on a user story, task, issue or epic by reference. Watcher identifiers (usernames, full names, or numeric user ids) are resolved exactly and case-insensitively against the project's members — no fuzzy matching.mode="add"merges into the existing watchers,mode="replace"sets them to exactly the given users (empty list clears all),mode="remove"drops the given users.manage_tags_by_ref_tool: Adds, replaces, or removes tags on a user story, task, issue or epic by reference.mode="add"(the default) merges into the existing tags,mode="replace"sets them to exactly the given list (empty list clears all),mode="remove"drops the given tags. Matching is case-insensitive and the spelling already stored in Taiga wins, so addingVoiceto an entity taggedvoiceis a no-op rather than a duplicate or a rename. Tags unknown to the project are created implicitly by Taiga and reported back increated_tags, so a typo is visible instead of silently becoming a permanent project tag; the project tag list is read before the write, because Taiga registers a new tag as part of that same save.created_tags: nullmeans the list could not be read and nothing was verified — distinct from[], which means nothing new was created.add_comment_by_ref_tool: Adds a comment to a user story, task or issue.add_attachment_by_ref_tool: Adds an attachment to a user story, task or issue by downloading a public URL (the server fetches it viarequests.get).create_attachment_upload_by_ref_tool: Returns a single-use URL for uploading a local file, so the bytes never pass through the model's context. The tool costs the same few hundred tokens whether the file is 2 KB or 20 MB — inlining a 500 KB screenshot as base64 in a tool call costs roughly 200k. Validates the target entity before minting the ticket, thenPOSTthe raw file bytes to the returnedupload_url(a ready-to-runcurlcommand comes back with it). The URL expires afterTAIGA_MCP_UPLOAD_TICKET_TTLseconds (default 600), is bound at issue time to one user, entity and filename, and is refused on second use. Bodies overTAIGA_MCP_MAX_UPLOAD_BYTES(default 25 MB) are rejected mid-stream, and at mostTAIGA_MCP_UPLOAD_CONCURRENCY(default 4) uploads reach Taiga at once — python-taiga's multipart POST reads the file back into memory to build the request, so both limits together are what keeps the pod inside its memory budget. Needs a shell on the calling client; clients without one (claude.ai on the web) should useadd_attachment_by_ref_toolwith a public URL.list_attachments_by_ref_tool: List all attachments on an entity with fresh signed download URLs. URL tokens from the Taiga UI/webhook diff expire after ~6 min; this tool re-mints them on every call.get_attachment_by_ref_tool: Fetch a specific attachment by ID and return its content base64-encoded inline. Refuses files larger thanTAIGA_MAX_INLINE_ATTACHMENT_BYTES(default 10 MB) — for larger files uselist_attachments_by_ref_toolandcurlthedownload_urlout-of-band.promote_issue_to_userstory_tool: Promotes an issue to a user story, preserving comments and history.list_custom_attributes_tool: Lists custom-attribute definitions for a project + entity type, including dropdownchoices(parsed) and the rawextrafield — so an agent can discover the valid options for a dropdown attribute before writing viaset_custom_attributes_tool. Since 2.23.0choicesis also filled when Taiga stores the options as a list (attributes created through the API) — before, those dropdowns listed no choices.set_custom_attributes_tool: Sets custom-attribute values on a user story, task, issue or epic, keyed by attribute name (any case) or id. Since 2.23.0 every value is checked first — an unknown attribute or a dropdown value outside its choices is a 400 that lists what is valid, and nothing is written — and all values go out in one PATCH.get_custom_attributes_tool: Reads custom-attribute values from a user story, task or issue. Takesfieldsandcompact.sort_kanban_by_rice_tool: Re-orders Kanban swimlanes using a RICE-style score. Closed status columns (Done, Cancelled, …) are skipped — re-ranking already-completed work has no value. Each entry incolumns_updatedcarriesstatus_namealongsidestatus_idso consumers don't need to round-trip through Taiga to translate the id.set_userstory_points_tool: Sets Taiga story points on a user story for one or more roles (Developer, UX, Design, …). Resolves role names + point values to internal IDs at runtime so it adapts to per-project scales. The only programmatic path to set the field thatsort_kanban_by_rice_toolreads as effort. The target role must be configured as computable in the project (Taiga admin → Members → Roles → "Compute story points for this role"); non-computable roles are rejected upfront with a 400 +non_computable_rolesdiagnostic, instead of letting Taiga's PATCH endpoint return its crypticInvalid role idserver-side rejection (which would otherwise surface as a generic 500 via this tool's exception wrapper).list_wiki_pages_tool: Lists wiki pages in a project.get_wiki_page_tool: Reads a wiki page by slug.create_wiki_page_tool: Creates a wiki page.update_wiki_page_tool: Updates a wiki page.whoami_tool: Returns the currently authenticated Taiga user.list_project_members_tool: Lists all members of a project with their roles (email opt-in). Takesfieldsandcompact.list_projects_tool(2.23.0): The projects you can see, optionally narrowed byslug_prefix, each withi_am_admin,i_am_memberand your ownmy_permissions. Takesfieldsandcompact.
Since 2.23.0 a write that Taiga refuses keeps Taiga's own code (for example 403 on a project where you may only read) instead of coming back as 500.
Token-lean reads
Every byte a tool returns lands in the calling model's context. The read tools above take two parameters (2.19.0) that keep that small:
fields: a list of dotted paths, e.g.["subject", "related.tasks.ref", "history.comment"]. Lists are walked element by element. A path whose first part is not a key of the answer fails the call and names the valid keys. A requested key that an element lacks comes back asnull.get_entity_by_ref_toolandget_kanban_board_toolfetch only what the paths need, so this saves Taiga requests too. Withfields, the answer echoes the request asquery.compact: single-line JSON, non-ASCII unescaped. Withoutfieldsit also drops nulls; requested nulls are kept, so "unassigned" stays distinguishable from "not asked for".
Measured on a Taiga project with ~1.6k user stories:
| Call | Answer | Time |
|---|---|---|
| One user story, default | 85.9 KB | 4.1 s |
The same story, fields + compact, no history |
1.7 KB | 1.0 s |
All open stories (include_closed=False), 1,484 cards |
85 KB | 17.8 s |
Six statuses (statuses=[...]), 47 cards |
6.1 KB | 0.9 s |
The same six statuses with columns.cards.tasks.*, 866 task summaries (2.20.0) |
89 KB | 1.7 s |
The same six statuses with tasks plus custom_attributes, last_activity_at and last_comment_at for two users (2.20.0) |
103 KB | 6.2 s |
Without either parameter every answer is byte-identical to 2.18.
Installation
pip install -U langchain-taiga
Environment Variables
For direct LangChain tool use, the toolkit, and stdio MCP mode (single-user, local credentials):
export TAIGA_URL="https://taiga.xyz.org/"
export TAIGA_API_URL="https://taiga.xyz.org/"
export TAIGA_USERNAME="username"
export TAIGA_PASSWORD="pw"
export OPENAI_API_KEY="..." # used by some tools' LLM-powered helpers
If TAIGA_USERNAME / TAIGA_PASSWORD are not set, the tools raise ValueError on call.
Optional: TAIGA_SMALL_LLM_MODEL overrides the model used to turn a natural-language query into search filters and to resolve member names (default gpt-5.6-luna). It is a classification step, not a reasoning one, so the smallest current tier is the default — set this to retune cost or latency without waiting for a release. Without OPENAI_API_KEY the package falls back to a local Ollama model and ignores this variable.
Remote MCP mode is different — see the Remote Mode section. There, end-users supply their own Taiga credentials interactively at the <mcp-path>/oauth/login form (e.g. https://your-server/mcp/oauth/login); only TAIGA_API_URL, TAIGA_URL, TAIGA_MCP_BASE_URL, and OPENAI_API_KEY are server-side env.
Usage
Direct Tool Usage
Each tool is a @tool-decorated function callable via .invoke({...}). A few representative examples:
from langchain_taiga.tools.taiga_tools import (
create_entity_tool,
search_entities_tool,
whoami_tool,
)
# Create — write
create_entity_tool.invoke({
"project_slug": "my-project",
"entity_type": "us",
"subject": "Add /metrics endpoint",
"status": "New",
"description": "Prometheus-scrape-friendly text format.",
"tags": ["backend", "observability"],
})
# Search — natural-language query, server-side caps + truncation flag in response
search_entities_tool.invoke({
"project_slug": "my-project",
"query": "open issues created after 2026-03-01",
"entity_type": "issue",
"max_results": 50,
})
# Whoami — verify auth wiring end-to-end
whoami_tool.invoke({})
For the full set of 25 tools see the list at the top of this README, the docstrings in taiga_tools.py, or just grab them all via the toolkit below.
Using the Toolkit
You can also use TaigaToolkit to automatically gather both tools:
from langchain_taiga.toolkits import TaigaToolkit
toolkit = TaigaToolkit()
tools = toolkit.get_tools()
MCP Server
The package ships an MCP server powered by fastmcp. All 25 tools above are exposed as MCP tools without changing their behaviour. There are two transport modes with different auth models:
| Mode | Transport | Auth | Use case |
|---|---|---|---|
| Stdio | stdin/stdout | env-var credentials | One developer running locally — Claude Desktop, Claude Code, VSCode local |
| Remote | HTTP (streamable) | OAuth 2.1 + PKCE + DCR | Multi-tenant — claude.ai Custom Connectors, VSCode Web, hosted teams |
Stdio Mode (single-user, local)
Run the server:
python -m langchain_taiga.mcp_server
Or without installing into your project (using uv):
uv run --with langchain-taiga python -m langchain_taiga.mcp_server
The Taiga credentials come from the TAIGA_USERNAME / TAIGA_PASSWORD env vars set in the wrapping process. All requests use the same single user.
VSCode
Add the following to your .vscode/mcp.json (or via the VSCode MCP settings UI):
{
"servers": {
"taiga": {
"command": "uv",
"args": [
"run",
"--with",
"langchain-taiga",
"python",
"-m",
"langchain_taiga.mcp_server"
],
"env": {
"TAIGA_API_URL": "${input:taiga_api_url}",
"TAIGA_URL": "${input:taiga_url}",
"TAIGA_USERNAME": "${input:taiga_username}",
"TAIGA_PASSWORD": "${input:taiga_password}"
}
}
},
"inputs": [
{
"id": "taiga_api_url",
"type": "promptString",
"description": "Taiga API URL (e.g. https://api.taiga.io)",
"password": false
},
{
"id": "taiga_url",
"type": "promptString",
"description": "Taiga Web URL (e.g. https://tree.taiga.io)",
"password": false
},
{
"id": "taiga_username",
"type": "promptString",
"description": "Taiga Username",
"password": false
},
{
"id": "taiga_password",
"type": "promptString",
"description": "Taiga Password",
"password": true
}
]
}
Claude Code
Add the Taiga MCP server via the CLI:
claude mcp add taiga -- uv run --with langchain-taiga python -m langchain_taiga.mcp_server
This adds the server to your project's .claude/mcp.json. Make sure the Taiga environment variables are set in your shell, or pass them explicitly:
claude mcp add taiga -e TAIGA_API_URL=https://api.taiga.io -e TAIGA_URL=https://tree.taiga.io -e TAIGA_USERNAME=your_user -e TAIGA_PASSWORD=your_pass -- uv run --with langchain-taiga python -m langchain_taiga.mcp_server
Alternatively, add the entry manually to .claude/mcp.json:
{
"mcpServers": {
"taiga": {
"command": "uv",
"args": [
"run",
"--with",
"langchain-taiga",
"python",
"-m",
"langchain_taiga.mcp_server"
],
"env": {
"TAIGA_API_URL": "https://api.taiga.io",
"TAIGA_URL": "https://tree.taiga.io",
"TAIGA_USERNAME": "your_user",
"TAIGA_PASSWORD": "your_pass"
}
}
}
}
Claude Desktop / GitHub Copilot Chat
Add a similar entry to your MCP configuration, pointing to
uv run --with langchain-taiga python -m langchain_taiga.mcp_server.
Remote Mode (multi-tenant, OAuth)
Run the server as a long-lived HTTP service:
TAIGA_API_URL=https://taiga.example.org \
TAIGA_URL=https://taiga.example.org \
TAIGA_MCP_BASE_URL=https://mcp.example.org/mcp \
OPENAI_API_KEY=sk-... \
langchain-taiga-mcp-remote
The server speaks MCP over the streamable-HTTP transport at the URL given by TAIGA_MCP_BASE_URL and implements:
- OAuth 2.1 + PKCE (S256) —
/authorize,/token - OAuth 2.1 refresh-token rotation with reuse-detection (2.5.0+) — connected MCP clients (claude.ai, VSCode, Claude Desktop) stay authenticated across hours without forcing the user back through the OAuth flow. Each refresh rotates the token; a replayed refresh token revokes the entire token family per OAuth 2.1 best practice for public clients.
- RFC 7591 Dynamic Client Registration —
/register - RFC 8414 / RFC 9728 discovery docs —
/.well-known/oauth-authorization-server[/<mcp-path>]and/.well-known/oauth-protected-resource[/<mcp-path>] - A login form at
<mcp-path>/oauth/loginwhere each user signs in with their own Taiga credentials. The Taiga JWT is then carried per-request via the MCPAccessTokenclaims and used by every tool call — so two users connected to the same server see only their own projects.
OAuth state lives in-process (per-pod in-memory dict). For production, run a single replica; users re-authorize after a Helm deploy or pod restart (refresh tokens are wiped along with everything else).
Connecting clients to a remote server
| Client | How to connect |
|---|---|
| claude.ai | Settings → Connectors → Add custom connector → URL https://your-server/mcp → sign in with Taiga creds. Anthropic docs. |
| VSCode (stable + Insiders + Web) | MCP UI → "Add server" → HTTP → https://your-server/mcp. DCR runs automatically; the redirect URIs (vscode.dev/redirect, insiders.vscode.dev/redirect, 127.0.0.1:<port>) are pre-allowed by the server's allowlist. |
| Claude Desktop | MCP config: "transport": {"type": "http", "url": "https://your-server/mcp"} |
| MCP Inspector | npx @modelcontextprotocol/inspector → URL https://your-server/mcp → "Quick OAuth Flow" |
Tests
The CI-aligned command (matches make test in CI):
make test
# expands to:
poetry run pytest --disable-socket --allow-unix-socket tests/unit_tests/
The --disable-socket flag blocks real network calls — the unit tests rely on it being on. Running pytest without it can hide tests that accidentally talk to live services.
License
Further Documentation
AGENTS.mdin this repo — onboarding for AI coding assistants and humans (test command, CI flow, MCP-SDK gotchas, architecture pointers).- Tool docstrings in
langchain_taiga/tools/taiga_tools.py. TaigaToolkit.- Remote OAuth bridge entry point:
langchain_taiga/remote_server.py. - Official Taiga Developer Docs: https://docs.taiga.io/api.html.
- LangChain GitHub for general LangChain usage.
- Model Context Protocol spec.
Metadata
Release files for langchain-taiga 2.23.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| langchain_taiga-2.23.0.tar.gz | 141.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langchain_taiga-2.23.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 282.7 kB
Release files / langchain_taiga-2.23.0.tar.gz
| Download URL | langchain_taiga-2.23.0.tar.gz |
|---|---|
| Size | 141.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2d44bb0d7bcd0b6c5c389b0ddbe8a4bed733e115011e9b3ffc3f2597b7663a29
|
|
BLAKE2b-256 checksum How to use checksums |
c359eb0b7e1ffc3c5dbe60cb07e146a9ed12144687a86b4a99e696389c51fdae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / langchain_taiga-2.23.0-py3-none-any.whl
| Download URL | langchain_taiga-2.23.0-py3-none-any.whl |
|---|---|
| Size | 141.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
95b5a0628af441879b0700ed62ef508aabdbec2bd36e822023edd2692ae8de92
|
|
BLAKE2b-256 checksum How to use checksums |
a9e47c2a09166982b6020749757eec66955ad0bfbac0cc96b9bee7a60f30ac4e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log