@kietdev/taiga-mcp 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +285 -2
  3. package/dist/config.js +59 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/format.js +104 -0
  6. package/dist/format.js.map +1 -0
  7. package/dist/index.js +22 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/remote/app.js +238 -0
  10. package/dist/remote/app.js.map +1 -0
  11. package/dist/remote/config.js +52 -0
  12. package/dist/remote/config.js.map +1 -0
  13. package/dist/remote/guard.js +120 -0
  14. package/dist/remote/guard.js.map +1 -0
  15. package/dist/remote/pages.js +472 -0
  16. package/dist/remote/pages.js.map +1 -0
  17. package/dist/remote/provider.js +211 -0
  18. package/dist/remote/provider.js.map +1 -0
  19. package/dist/remote/seal.js +50 -0
  20. package/dist/remote/seal.js.map +1 -0
  21. package/dist/remote/taiga-auth.js +70 -0
  22. package/dist/remote/taiga-auth.js.map +1 -0
  23. package/dist/remote.js +23 -0
  24. package/dist/remote.js.map +1 -0
  25. package/dist/server.js +64 -0
  26. package/dist/server.js.map +1 -0
  27. package/dist/taiga/client.js +1274 -0
  28. package/dist/taiga/client.js.map +1 -0
  29. package/dist/taiga/files.js +331 -0
  30. package/dist/taiga/files.js.map +1 -0
  31. package/dist/taiga/http.js +237 -0
  32. package/dist/taiga/http.js.map +1 -0
  33. package/dist/taiga/types.js +6 -0
  34. package/dist/taiga/types.js.map +1 -0
  35. package/dist/tools/add-comment.js +24 -0
  36. package/dist/tools/add-comment.js.map +1 -0
  37. package/dist/tools/create-item.js +63 -0
  38. package/dist/tools/create-item.js.map +1 -0
  39. package/dist/tools/file-view.js +300 -0
  40. package/dist/tools/file-view.js.map +1 -0
  41. package/dist/tools/get-attachment.js +76 -0
  42. package/dist/tools/get-attachment.js.map +1 -0
  43. package/dist/tools/get-item.js +120 -0
  44. package/dist/tools/get-item.js.map +1 -0
  45. package/dist/tools/get-project.js +47 -0
  46. package/dist/tools/get-project.js.map +1 -0
  47. package/dist/tools/get-sprint.js +56 -0
  48. package/dist/tools/get-sprint.js.map +1 -0
  49. package/dist/tools/list-activity.js +102 -0
  50. package/dist/tools/list-activity.js.map +1 -0
  51. package/dist/tools/list-epics.js +58 -0
  52. package/dist/tools/list-epics.js.map +1 -0
  53. package/dist/tools/list-issues.js +78 -0
  54. package/dist/tools/list-issues.js.map +1 -0
  55. package/dist/tools/list-projects.js +22 -0
  56. package/dist/tools/list-projects.js.map +1 -0
  57. package/dist/tools/list-sprints.js +46 -0
  58. package/dist/tools/list-sprints.js.map +1 -0
  59. package/dist/tools/list-tasks.js +84 -0
  60. package/dist/tools/list-tasks.js.map +1 -0
  61. package/dist/tools/list-user-stories.js +85 -0
  62. package/dist/tools/list-user-stories.js.map +1 -0
  63. package/dist/tools/search.js +83 -0
  64. package/dist/tools/search.js.map +1 -0
  65. package/dist/tools/shared.js +90 -0
  66. package/dist/tools/shared.js.map +1 -0
  67. package/dist/tools/update-item.js +74 -0
  68. package/dist/tools/update-item.js.map +1 -0
  69. package/dist/tools/wiki.js +62 -0
  70. package/dist/tools/wiki.js.map +1 -0
  71. package/dist/tools/written.js +18 -0
  72. package/dist/tools/written.js.map +1 -0
  73. package/package.json +78 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kiệt Nguyễn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,286 @@
1
- # Temporary Holding Version
1
+ # taiga-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/kiet0807/taiga-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/kiet0807/taiga-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@kietdev/taiga-mcp)](https://www.npmjs.com/package/@kietdev/taiga-mcp)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
+
7
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for [Taiga](https://taiga.io). It lets an AI assistant such as Claude, ChatGPT, Copilot or Cursor answer questions about your Taiga projects without you opening Taiga: sprints, user stories, tasks, issues, epics and the wiki. It reads by default; if you switch it on, it can also create items, change them and comment. It can never delete anything.
8
+
9
+ <!-- A short demo goes here: ![Asking Claude about a Taiga sprint](docs/demo.gif) -->
10
+
11
+ ## Try it in 30 seconds
12
+
13
+ Nothing to install. In Claude, open Settings → Connectors → Add custom connector and paste:
14
+
15
+ ```text
16
+ https://taiga.kietnguyen.io.vn/mcp
17
+ ```
18
+
19
+ Sign in with your own Taiga account, on Taiga cloud or any Taiga reachable from the internet, then ask:
20
+
21
+ - "What is left in the current sprint?"
22
+ - "What does #123 say, and who has it?"
23
+ - "Read the spreadsheet attached to #456 and list the rows marked urgent."
24
+
25
+ Other apps (ChatGPT, Claude Code, Cursor, VS Code, Gemini CLI, Codex, Windsurf): see [Connecting an app](#connecting-an-app). Your password is passed to Taiga once and never stored ([how signing in works](#how-signing-in-works)); if you would rather nobody else's server saw it, [run it on your machine](#setup) or [host your own](#hosted-server).
26
+
27
+ ## Why this one
28
+
29
+ - **Reads what is attached.** PDFs, Word, Excel, PowerPoint, CSV and pictures attached to an item, in parts when they are large.
30
+ - **Nothing to install for a team.** One address, and each person signs in with their own Taiga account (OAuth). No keys to hand around, no database.
31
+ - **Writes only when allowed, never deletes.** Creating, changing and commenting are off until switched on, refuse to overwrite someone else's change, and show every field before and after.
32
+ - **Sprints the way a team talks about them.** Points done, stories done, burndown day by day, and what happened this week, by person.
33
+ - **Names, not ids.** Statuses, people, sprints and tags by name (no accents needed), items by `#123`; errors say what to try next.
34
+ - **Any Taiga.** Taiga cloud or self-hosted (for a Taiga inside a company network, [host your own](#hosted-server)); sign-in pages in English and Vietnamese.
35
+
36
+ How it works inside, flow by flow, with links into the code: [docs/how-it-works.md](docs/how-it-works.md).
37
+
38
+ It runs in two ways:
39
+
40
+ - **On your machine** (stdio), for one person, with the Taiga account in its settings. See [Setup](#setup).
41
+ - **Hosted** (HTTP), for a team, including people who never open a terminal: they paste one address into their AI assistant and sign in with their own Taiga account. See [Hosted server](#hosted-server).
42
+
43
+ **Status:** fourteen tools that read and three that write (off unless switched on), covered by tests against a fake Taiga; the reading tools are also checked against a public project on Taiga cloud. A hosted instance runs at [taiga.kietnguyen.io.vn](https://taiga.kietnguyen.io.vn).
44
+
45
+ ## Tools
46
+
47
+ | Tool | What it answers |
48
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `list_projects` | The projects you are a member of, with their slugs |
50
+ | `get_project` | One project: members and roles, statuses (and which count as closed), issue types, severities, priorities, tags, fields |
51
+ | `list_sprints` | Sprints with dates, stories done, points done, and which one runs today |
52
+ | `get_sprint` | One sprint: stories, tasks and points done, and the burndown day by day up to today |
53
+ | `list_user_stories` | Stories with status, assignee, points and sprint |
54
+ | `list_tasks` | Tasks with status, assignee, and the story and sprint they belong to |
55
+ | `list_issues` | Issues with type, status, severity, priority and assignee |
56
+ | `list_epics` | Epics with status, assignee and how many of their stories are done |
57
+ | `get_item` | One story, task, issue or epic in full: dates, description, custom fields, files, watchers, votes, comments, its tasks, and its history page by page |
58
+ | `get_attachment` | What is in a file attached to an item: text, tables, pages or the picture itself, in parts when it is large |
59
+ | `list_activity` | What happened in a project, newest first: who created, changed, commented on or deleted what, by person, day, kind or item |
60
+ | `search` | Words across a whole project: stories, tasks, issues, epics and wiki pages at once |
61
+ | `list_wiki_pages` | The wiki pages of a project |
62
+ | `get_wiki_page` | The text of one wiki page |
63
+
64
+ The four list tools for items share their filters:
65
+
66
+ | Filter | Meaning |
67
+ | ------------------------------- | ------------------------------------------------------------------------------------------ |
68
+ | `status`, `closed` | A status by name, or open/closed in general (by whether the status counts as closed) |
69
+ | `assigned_to_me`, `assigned_to` | The signed-in user, or anyone by name (part of a name, no accents needed), or `unassigned` |
70
+ | `search` | Words in the title, description and tags |
71
+ | `tag` | One tag |
72
+ | `created_from`, `created_to` | Days the item was created in, `YYYY-MM-DD` |
73
+ | `finished_from`, `finished_to` | Days the item was closed in (stories, tasks, issues; Taiga keeps no such date for epics) |
74
+ | `limit`, `page` | 30 items a page by default, at most 100; `page` reads the rest of a long list |
75
+
76
+ Plus `sprint` (a name, `current` or `backlog`) and `epic` on stories, `story` and `sprint` on tasks, `type`, `severity` and `priority` on issues.
77
+
78
+ How the tools are meant to be used by a model:
79
+
80
+ - **Names in, names out.** `project` is the slug from `list_projects`; statuses, severities, sprints, tags and people are given by name (case does not matter); items are addressed by the `#123` reference number. Internal ids never appear.
81
+ - **Counting needs no paging.** Every list starts with the number of all matches (`Acme Billing · assigned to me · finished 2026-10-01 to 2026-10-31: 14 tasks`), so "how many" is one call with a small `limit`.
82
+ - **Long lists come in pages.** An answer that does not hold everything says which part it is (`31 to 60 shown, page 2 of 5`) and how to ask for the next.
83
+ - **`get_item` needs no type.** Taiga numbers stories, tasks, issues and epics of a project from one sequence, so the number says which kind it is.
84
+ - **Short answers.** Long descriptions, comments and wiki pages are cut, with a note saying how much is missing.
85
+ - **Activity is read backwards.** Taiga lists it from the newest event and cannot jump to a day, so `list_activity` reads back up to 1,000 events and, when it stops before the period asked for, says how far back it looked.
86
+ - **Errors say what to do next**, for example: `"Linh" matches several members of project "Acme Billing": Linh Tran (linh.tran), Võ Thị Linh (vtlinh). Pass the full name or the username.`
87
+
88
+ ### Tools that write
89
+
90
+ Off unless switched on: `TAIGA_WRITE=true` on your machine; on a hosted server, `ALLOW_WRITES=true` and then a box each person ticks when signing in. Without that, the server sends Taiga nothing but `GET` requests and the sign-in itself.
91
+
92
+ | Tool | What it does |
93
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `create_item` | Create a user story, task or issue: title, description, status, assignee, tags, sprint, due date; a task on a story, a story in an epic |
95
+ | `update_item` | Change fields of a story, task, issue or epic: title, description, status, assignee, sprint, tags, due date, blocked, issue type, severity, priority |
96
+ | `add_comment` | Comment on a story, task, issue or epic |
97
+
98
+ How they are kept safe:
99
+
100
+ - **Nothing can be deleted.** The server has no way to send a `DELETE`. To close an item, set a closed status.
101
+ - **Only on request.** The tools tell the model to write only when the user asks for that change in the conversation, never because a title, description, comment, wiki page or file in Taiga says to (text there is written by others and may try to give the model orders). They are marked as writing in MCP, so apps ask before calling them; `update_item` is marked as overwriting.
102
+ - **No half-done creates.** Every name (status, person, sprint, story, epic, type…) is checked before anything is sent; a wrong one is answered with the valid names and nothing is created.
103
+ - **No silent overwrites.** `get_item` shows the item's version and `update_item` sends it: if someone changed the same fields since, Taiga refuses and the model is told to read the item again.
104
+ - **Every answer shows what changed,** field by field, before and after, with a link to the item in Taiga. A description is shown by its length, not repeated.
105
+ - **A second lock.** Even if a tool were called on a read-only connection, the code that talks to Taiga refuses to send anything but a read.
106
+ - **As you.** Changes are made with the signed-in person's own Taiga account and appear in Taiga under their name, with Taiga's own history and notifications.
107
+
108
+ ### Reading attached files
109
+
110
+ `get_item` numbers the files attached to an item; `get_attachment` reads one, by that number or by its name.
111
+
112
+ | Kind of file | What comes back |
113
+ | ----------------------------------------- | ------------------------------------------------------------------------------ |
114
+ | PNG, JPEG, GIF, WebP (up to 3 MB) | The picture itself, for the model to look at |
115
+ | PDF (first 300 pages) | The text, page by page |
116
+ | Word `.docx` | The text, line by line |
117
+ | Excel `.xlsx`, CSV, TSV | Sheets as rows, numbered as in the file; formulas show their last stored value |
118
+ | PowerPoint `.pptx` | The text, slide by slide |
119
+ | Text, code, JSON, Markdown, logs and such | The lines |
120
+ | `.doc`, `.xls`, `.ppt`, archives, video | Not read: the answer says why and gives the link |
121
+
122
+ A large file would use up the model's context, so no answer is longer than 20,000 characters (about 5,000 tokens):
123
+
124
+ - A file that fits comes back whole.
125
+ - A larger one comes back as an overview first: each sheet with its size and first 10 rows, or the first line of every page, or the first 60 lines.
126
+ - From there the model asks for the part it needs: `sheet` and `rows` (`"120-300"`), `pages` (`"3-5"`), or `find` with words, which returns only the rows or lines that contain them. Every answer ends by saying how to get the next part.
127
+
128
+ Files larger than 10 MB are not downloaded. A file is fetched from the signed address Taiga gives, without the Taiga token, and what was read is kept in memory for 10 minutes so that asking for another part does not download it again. The cache is keyed by the checksum Taiga reports for the file: whoever can name a checksum has been shown that very file by Taiga.
129
+
130
+ ## Setup
131
+
132
+ Requires Node.js 20.6 or newer. The server is on npm as [`@kietdev/taiga-mcp`](https://www.npmjs.com/package/@kietdev/taiga-mcp); `npx` fetches and runs it, so there is nothing to clone.
133
+
134
+ Claude Code:
135
+
136
+ ```bash
137
+ claude mcp add taiga \
138
+ -e TAIGA_URL=https://api.taiga.io \
139
+ -e TAIGA_USERNAME=you -e TAIGA_PASSWORD='your password' \
140
+ -- npx -y @kietdev/taiga-mcp
141
+ ```
142
+
143
+ Claude Desktop (`claude_desktop_config.json`), Cursor (`~/.cursor/mcp.json`) and most other apps take the same block:
144
+
145
+ ```json
146
+ {
147
+ "mcpServers": {
148
+ "taiga": {
149
+ "command": "npx",
150
+ "args": ["-y", "@kietdev/taiga-mcp"],
151
+ "env": {
152
+ "TAIGA_URL": "https://api.taiga.io",
153
+ "TAIGA_USERNAME": "you",
154
+ "TAIGA_PASSWORD": "your password"
155
+ }
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ For a self-hosted Taiga, `TAIGA_URL` is usually the address you open Taiga at. Add `"TAIGA_WRITE": "true"` to let the AI create, change and comment.
162
+
163
+ ### From source
164
+
165
+ Requires [pnpm](https://pnpm.io).
166
+
167
+ ```bash
168
+ pnpm install
169
+ pnpm build
170
+ cp .env.example .env # then fill in TAIGA_URL and how to sign in
171
+ ```
172
+
173
+ | Variable | Meaning |
174
+ | ------------------------------------ | -------------------------------------------------------------------------------- |
175
+ | `TAIGA_URL` | Address of the Taiga API host, without `/api/v1` |
176
+ | `TAIGA_USERNAME`, `TAIGA_PASSWORD` | Sign in with a password; the server renews its tokens by itself |
177
+ | `TAIGA_TOKEN`, `TAIGA_REFRESH_TOKEN` | Or use tokens you already have; without a password they cannot be replaced |
178
+ | `TAIGA_WRITE` | `true` adds the tools that create and change items, and comment (off by default) |
179
+
180
+ The same variables go in the `env` block above when you run it with `npx`. Credentials stay in `.env` on your machine. The file is git-ignored and the server never prints them.
181
+
182
+ ## Try it from source
183
+
184
+ Open the MCP Inspector, a browser page where you can call each tool by hand:
185
+
186
+ ```bash
187
+ pnpm inspect
188
+ ```
189
+
190
+ Add it to Claude Code (use absolute paths):
191
+
192
+ ```bash
193
+ claude mcp add taiga -- node --env-file=/path/to/taiga-mcp/.env /path/to/taiga-mcp/dist/index.js
194
+ ```
195
+
196
+ Then ask, for example:
197
+
198
+ - "Which Taiga projects am I in?"
199
+ - "What is left in the current sprint of Acme Billing?"
200
+ - "What does #123 say, and who has it?"
201
+ - "Which epics are furthest from done?"
202
+
203
+ ## Hosted server
204
+
205
+ The same tools over HTTP, for many people at once. Nobody installs anything: each person adds the server's address to an MCP app (in Claude: Settings → Connectors → Add custom connector), signs in with their own Taiga account on the page that opens, and from then on sees exactly what Taiga lets that account see.
206
+
207
+ A hosted server is set up in one of two ways:
208
+
209
+ - **For one Taiga** (`TAIGA_URL`): a team's own server. The sign-in page asks for a username and a password.
210
+ - **For any Taiga** (`ANY_TAIGA=true`): a public server. The sign-in page also asks for the address of the person's Taiga: Taiga cloud, or the address they open their company's Taiga at.
211
+
212
+ ```bash
213
+ pnpm install && pnpm build
214
+ ANY_TAIGA=true \
215
+ PUBLIC_URL=https://taiga-mcp.example.com \
216
+ MCP_SECRET="$(openssl rand -base64 48)" \
217
+ pnpm start:http
218
+ ```
219
+
220
+ | Variable | Meaning |
221
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | `TAIGA_URL` | The Taiga people sign in to. With `ANY_TAIGA` it only pre-fills the address field, and may be left out |
223
+ | `ANY_TAIGA` | `true` lets each person type the address of their own Taiga |
224
+ | `ALLOW_WRITES` | `true` offers each person, at sign-in, to let their app create and change items (off by default). Turning it off takes writing away from every connection at once |
225
+ | `PUBLIC_URL` | The address people reach this server at, without a path. Must be `https://` (except on `localhost`) |
226
+ | `MCP_SECRET` | At least 32 random characters. Encrypts everything the server hands out; changing it signs everyone out |
227
+ | `PORT` | Port to listen on (default 3000) |
228
+ | `TRUST_PROXY` | How many proxies sit in front of the server (default 1 when hosted, 0 on `localhost`) |
229
+
230
+ People connect their app to `PUBLIC_URL` + `/mcp`. Opening `PUBLIC_URL` itself in a browser shows the steps, in English or Vietnamese, for Claude and for other apps. To try it on your machine: put `PUBLIC_URL=http://localhost:3000` and `MCP_SECRET` in `.env` and run `pnpm dev:http`.
231
+
232
+ ### Connecting an app
233
+
234
+ | App | How |
235
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------- |
236
+ | Claude (web, desktop) | Settings → Connectors → Add custom connector, paste the address |
237
+ | Claude Code | `claude mcp add --transport http taiga <address>`, then `/mcp` to sign in |
238
+ | ChatGPT | Settings → Apps & Connectors → Advanced settings → Developer mode, then Create with the address and OAuth |
239
+ | VS Code (GitHub Copilot) | `.vscode/mcp.json`: `{ "servers": { "taiga": { "type": "http", "url": "<address>" } } }` |
240
+ | Cursor | `~/.cursor/mcp.json`: `{ "mcpServers": { "taiga": { "url": "<address>" } } }` |
241
+ | Gemini CLI | `~/.gemini/settings.json`: `{ "mcpServers": { "taiga": { "httpUrl": "<address>" } } }`, then `/mcp auth taiga` |
242
+ | Codex CLI | `codex mcp add taiga --url <address>`, then `codex mcp login taiga` |
243
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json`: `{ "mcpServers": { "taiga": { "serverUrl": "<address>" } } }` |
244
+
245
+ Each app finds the sign-in by itself (OAuth discovery); there is no key to paste. These steps follow each app's documentation; so far the hosted server has been tried with the MCP SDK's own client only.
246
+
247
+ ### Where to run it
248
+
249
+ Any host that runs a Node.js process works: `pnpm build`, then `pnpm start:http` with the variables above.
250
+
251
+ On Vercel no command is needed: the root `app.js` exports the Express app from the compiled `dist/`, which Vercel runs as one function after its own `pnpm build`. Vercel serves `public/` (icons, the link-preview image, `robots.txt`) itself; elsewhere the app does. Import the repository, set `PUBLIC_URL`, `MCP_SECRET` and `ANY_TAIGA` (or `TAIGA_URL`) as environment variables, and add the domain. The server keeps nothing between requests, so it does not mind being started and stopped; the sign-in limit and the single use of a code (below) are then per running instance.
252
+
253
+ ### How signing in works
254
+
255
+ - **Taiga has no OAuth of its own**, so the server puts a standard OAuth 2.1 sign-in (PKCE, dynamic client registration) in front of Taiga's username and password. People type their Taiga password into this server's page. It is passed to Taiga once, over HTTPS, and is never stored or logged. Run the server only where the people using it would trust it with that.
256
+ - **The sign-in page says where the person will be sent back to.** Any app can register itself under any name, so the name alone proves nothing; the host that receives the result does.
257
+ - **Nothing is stored.** There is no database. The Taiga address and the Taiga tokens a sign-in produces are encrypted (AES-256-GCM, key derived from `MCP_SECRET`) into the tokens the app holds; the server can read them only while answering that app's request.
258
+ - **Each request stands alone.** A request is answered with the Taiga token of the person who sent it, by a server and a Taiga client created for that request, so nothing one person reads is cached for another.
259
+ - **Sessions follow Taiga's.** An access token lasts as long as the Taiga token inside it (24 hours by default) and is renewed through Taiga's own refresh token (8 days by default). After that the person signs in again.
260
+ - **A typed address cannot lead inside.** With `ANY_TAIGA`, the server calls hosts that strangers name. Only host names over HTTPS are accepted, and every connection is checked when it is made: a name that resolves to a private, loopback or link-local address is refused, including after a redirect. A server for one Taiga skips this check, so that a company can point it at a Taiga on its own network.
261
+ - **Limits of having no storage:** a connection cannot be revoked from the server before its tokens run out (short of changing `MCP_SECRET`, which ends everyone's), and an authorization code is kept from being used twice only within one running process. PKCE covers the rest.
262
+ - Sign-in attempts are limited to 10 per 15 minutes per address.
263
+ - **Writing is a separate choice.** With `ALLOW_WRITES`, the sign-in page has a box, unticked by default, that lets the app create items, change them and comment. The person's choice is sealed into their tokens (scope `read` or `read write`) and survives renewal. Apps that registered before writing existed still see the box: the person decides, not the scope the app asked for.
264
+
265
+ ## Development
266
+
267
+ ```bash
268
+ pnpm check # types, lint, formatting, tests, build
269
+ pnpm test # tests only; Taiga is faked, no network or account needed
270
+ ```
271
+
272
+ - `src/taiga/http.ts` is the only code that talks to Taiga: sign-in, token renewal, paging, error messages.
273
+ - `src/taiga/client.ts` asks Taiga what the tools need: it turns slugs and names into ids, and trims the answers.
274
+ - `src/tools/` holds one file per tool: its name, the description and arguments the model reads, and how the answer is worded.
275
+ - `src/taiga/files.ts` turns an attached file into pages, sheets or lines; `src/tools/file-view.ts` decides which part of it fits in one answer.
276
+ - `src/server.ts` registers the tools; `src/index.ts` connects the server to stdio.
277
+ - `src/remote/` is the hosted server: `app.ts` (routes), `provider.ts` (the OAuth steps), `seal.ts` (encryption of what is handed out), `taiga-auth.ts` (sign-in and renewal at Taiga), `guard.ts` (what a typed Taiga address may be, and connections that stay on the public internet), `pages.ts` (the sign-in and front pages). `src/remote.ts` starts it; the root `app.js` is the same app, taken from `dist/`, for hosts that want an exported Express app.
278
+ - `public/` holds the icons, the link-preview image and `robots.txt`; `design/og.html` is the source of the preview image.
279
+ - `test/acme.ts` is the invented project the tests run against.
280
+ - CI (`.github/workflows/ci.yml`) runs `pnpm check` on every push and pull request.
281
+
282
+ The stdio server speaks MCP on stdout, so nothing else may be printed there. Diagnostics go to stderr.
283
+
284
+ ## A note on data
285
+
286
+ What a tool returns is sent to the AI model you connect. Before pointing the server at a company Taiga, make sure that is acceptable where you work, and ask before switching on writing there too. The same goes for hosting it for colleagues: whoever runs the hosted server is trusted with their passwords in passing and with what they read.
package/dist/config.js ADDED
@@ -0,0 +1,59 @@
1
+ /** A problem with the environment variables. The message says what to set; it never echoes a secret. */
2
+ export class ConfigError extends Error {
3
+ name = 'ConfigError';
4
+ }
5
+ /** An unset variable and an empty one (`TAIGA_TOKEN=` in a .env file) mean the same thing. */
6
+ export function read(env, key) {
7
+ const value = env[key]?.trim();
8
+ return value ? value : undefined;
9
+ }
10
+ /** A switch: true or false (also 1 or 0), off when unset. */
11
+ export function flag(env, key) {
12
+ const raw = read(env, key)?.toLowerCase();
13
+ if (raw === undefined)
14
+ return false;
15
+ if (!['true', 'false', '1', '0'].includes(raw))
16
+ throw new ConfigError(`${key} must be true or false.`);
17
+ return raw === 'true' || raw === '1';
18
+ }
19
+ export function normalizeBaseUrl(raw) {
20
+ let url;
21
+ try {
22
+ url = new URL(raw);
23
+ }
24
+ catch {
25
+ throw new ConfigError(`TAIGA_URL is not a valid address: "${raw}". Example: https://api.taiga.io`);
26
+ }
27
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
28
+ throw new ConfigError('TAIGA_URL must start with https:// (or http:// for a local Taiga).');
29
+ }
30
+ // People paste the address with or without the API path; both mean the same host.
31
+ const path = url.pathname.replace(/\/+$/, '').replace(/\/api\/v1$/, '');
32
+ return `${url.origin}${path}`;
33
+ }
34
+ export function loadConfig(env = process.env) {
35
+ const rawUrl = read(env, 'TAIGA_URL');
36
+ if (!rawUrl) {
37
+ throw new ConfigError('TAIGA_URL is not set. Set it to your Taiga API host, e.g. https://api.taiga.io');
38
+ }
39
+ const baseUrl = normalizeBaseUrl(rawUrl);
40
+ const username = read(env, 'TAIGA_USERNAME');
41
+ const password = read(env, 'TAIGA_PASSWORD');
42
+ const token = read(env, 'TAIGA_TOKEN');
43
+ const refreshToken = read(env, 'TAIGA_REFRESH_TOKEN');
44
+ if ((username === undefined) !== (password === undefined)) {
45
+ throw new ConfigError('Set both TAIGA_USERNAME and TAIGA_PASSWORD, or neither.');
46
+ }
47
+ if (username === undefined && token === undefined) {
48
+ throw new ConfigError('No way to sign in: set TAIGA_USERNAME and TAIGA_PASSWORD, or TAIGA_TOKEN.');
49
+ }
50
+ const write = flag(env, 'TAIGA_WRITE');
51
+ return {
52
+ baseUrl,
53
+ ...(username !== undefined && password !== undefined ? { credentials: { username, password } } : {}),
54
+ ...(token !== undefined ? { token } : {}),
55
+ ...(refreshToken !== undefined ? { refreshToken } : {}),
56
+ ...(write ? { write } : {}),
57
+ };
58
+ }
59
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/format.js ADDED
@@ -0,0 +1,104 @@
1
+ import { TaigaError } from './taiga/http.js';
2
+ /** A tool's answer as plain text: what the model reads. */
3
+ export function text(value) {
4
+ return { content: [{ type: 'text', text: value }] };
5
+ }
6
+ /**
7
+ * A failed tool call. It is returned, not thrown, and flagged `isError`, so the model sees
8
+ * what went wrong and can try something else or tell the user.
9
+ */
10
+ export function failure(error) {
11
+ const message = error instanceof TaigaError
12
+ ? error.message
13
+ : `Unexpected error: ${error instanceof Error ? error.message : String(error)}`;
14
+ return { isError: true, content: [{ type: 'text', text: message }] };
15
+ }
16
+ /** Run a tool handler that answers in text; whatever it throws becomes a readable tool error. */
17
+ export function answer(handler) {
18
+ return async (args) => {
19
+ try {
20
+ return text(await handler(args));
21
+ }
22
+ catch (error) {
23
+ return failure(error);
24
+ }
25
+ };
26
+ }
27
+ export function plural(count, one, many = `${one}s`) {
28
+ return `${count} ${count === 1 ? one : many}`;
29
+ }
30
+ /**
31
+ * A list as the model reads it: a heading that repeats which filters were applied, one line
32
+ * per item, and, when the list does not fit in one answer, which part this is and how to get
33
+ * the rest.
34
+ */
35
+ export function list(options) {
36
+ const { lines, total, noun, limit, page } = options;
37
+ const scope = options.scope.filter(Boolean).join(' · ');
38
+ if (lines.length === 0)
39
+ return `${scope}: no ${noun[1]} found.`;
40
+ if (page === 1 && lines.length >= total)
41
+ return `${scope}: ${plural(total, ...noun)}.\n${lines.join('\n')}`;
42
+ const first = (page - 1) * limit + 1;
43
+ const last = first + lines.length - 1;
44
+ const pages = Math.max(page, Math.ceil(total / limit));
45
+ const heading = `${scope}: ${plural(total, ...noun)}, ${first} to ${last} shown (page ${page} of ${pages}).`;
46
+ const rest = last < total
47
+ ? `${total - last} more after these. For the next ones call again with \`page\`: ${page + 1} and the same \`limit\` and filters, or narrow the filters.`
48
+ : 'This is the last page.';
49
+ return `${heading}\n${lines.join('\n')}\n\n${rest}`;
50
+ }
51
+ /** "assigned to Linh Tran", for the heading of a list. */
52
+ export const assignedScope = (who) => who !== undefined && `assigned to ${who}`;
53
+ /** `finished 2026-03-01 to 2026-03-31`, for the heading of a list. Either end may be open. */
54
+ export function dateScope(what, from, to) {
55
+ if (from !== undefined && to !== undefined)
56
+ return `${what} ${from} to ${to}`;
57
+ if (from !== undefined)
58
+ return `${what} from ${from}`;
59
+ return to !== undefined && `${what} until ${to}`;
60
+ }
61
+ /** `matching "invoice export"`, for the heading of a list. */
62
+ export const searchScope = (words) => !!words?.trim() && `matching "${words.trim()}"`;
63
+ export const DEFAULT_LIMIT = 30;
64
+ export const MAX_LIMIT = 100;
65
+ /** One line of a list: the parts that exist, joined. */
66
+ export function line(ref, subject, ...parts) {
67
+ return [`- #${ref} ${oneLine(subject)}`, ...parts.filter(Boolean)].join(' · ');
68
+ }
69
+ const oneLine = (value) => value.replace(/\s+/g, ' ').trim();
70
+ /** Status as text, saying so when it counts as closed (names like "Ready for test" do not tell). */
71
+ export function status(item) {
72
+ return item.closed ? `${item.status} (closed)` : item.status;
73
+ }
74
+ export const assignee = (name) => name ?? 'unassigned';
75
+ /** Words for the `closed` filter in a heading. */
76
+ export function closedScope(closed) {
77
+ return closed === undefined ? undefined : closed ? 'closed only' : 'open only';
78
+ }
79
+ /**
80
+ * "35 stories, 34% done". Taiga counts a half-finished story as a fraction, so a count of done
81
+ * stories ("5.5 of 21") reads oddly; a percentage of the whole does not.
82
+ */
83
+ export function progress(done, total) {
84
+ if (total === 0)
85
+ return 'no stories';
86
+ return `${total} ${total === 1 ? 'story' : 'stories'}, ${Math.round((done / total) * 100)}% done`;
87
+ }
88
+ /** A file's size as people say it: "84 KB", "1.2 MB". */
89
+ export function fileSize(bytes) {
90
+ if (bytes < 1024)
91
+ return `${bytes} B`;
92
+ if (bytes < 1024 * 1024)
93
+ return `${Math.round(bytes / 1024)} KB`;
94
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
95
+ }
96
+ /** For matching what people type: case and accents do not matter, so "kiet" finds "Kiệt". */
97
+ export const fold = (value) => value.trim().toLowerCase().normalize('NFD').replace(/\p{M}/gu, '').replaceAll('đ', 'd');
98
+ /** Long text cut to `max` characters, saying how much was left out. */
99
+ export function truncate(value, max) {
100
+ if (value.length <= max)
101
+ return value;
102
+ return `${value.slice(0, max).trimEnd()}… [${value.length - max} more characters not shown]`;
103
+ }
104
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { ConfigError, loadConfig } from './config.js';
4
+ import { createServer, SERVER_INFO } from './server.js';
5
+ import { TaigaClient } from './taiga/client.js';
6
+ // stdout belongs to the MCP protocol. Everything meant for a person goes to stderr.
7
+ try {
8
+ const config = loadConfig();
9
+ const server = createServer(new TaigaClient(config), {
10
+ howToAllowWrites: 'set TAIGA_WRITE=true where this server is configured, then restart it.',
11
+ });
12
+ await server.connect(new StdioServerTransport());
13
+ console.error(`${SERVER_INFO.name} ${SERVER_INFO.version} ready (Taiga at ${config.baseUrl}; ` +
14
+ `${config.write ? 'creating and changing items is ON' : 'read-only'})`);
15
+ }
16
+ catch (error) {
17
+ console.error(error instanceof ConfigError
18
+ ? `taiga-mcp: ${error.message}`
19
+ : `taiga-mcp failed to start: ${String(error)}`);
20
+ process.exit(1);
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,oFAAoF;AACpF,IAAI,CAAC;IACH,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE;QACnD,gBAAgB,EAAE,wEAAwE;KAC3F,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CACX,GAAG,WAAW,CAAC,IAAI,IAAI,WAAW,CAAC,OAAO,oBAAoB,MAAM,CAAC,OAAO,IAAI;QAC9E,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,WAAW,GAAG,CACzE,CAAC;AACJ,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,KAAK,CACX,KAAK,YAAY,WAAW;QAC1B,CAAC,CAAC,cAAc,KAAK,CAAC,OAAO,EAAE;QAC/B,CAAC,CAAC,8BAA8B,MAAM,CAAC,KAAK,CAAC,EAAE,CAClD,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}