@taiga-ai/mcp-server 0.9.0-rc.403 → 0.10.0-rc.405

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.
package/dist/index.js CHANGED
@@ -175,6 +175,29 @@ var TaigaClient = class {
175
175
  async getBacklogItem(itemId) {
176
176
  return this.request("GET", `/backlog/${seg(itemId)}`);
177
177
  }
178
+ async createBacklogItem(productId, body) {
179
+ return this.request("POST", `/products/${seg(productId)}/backlog`, {
180
+ ...body,
181
+ productId
182
+ });
183
+ }
184
+ /** A person's move on the line board: the column dropped into, and what to follow there. */
185
+ async transitionInitiative(itemId, body) {
186
+ return this.request("PUT", `/backlog/${seg(itemId)}/state`, body);
187
+ }
188
+ /** Answer the needs-human flag: plan again, resume the run, or clear a refused start. */
189
+ async resumeInitiative(itemId) {
190
+ return this.request("POST", `/backlog/${seg(itemId)}/resume`);
191
+ }
192
+ async getActivePlan(itemId) {
193
+ return this.request("GET", `/backlog/${seg(itemId)}/plans/active`);
194
+ }
195
+ async approvePlan(planId) {
196
+ return this.request("POST", `/plans/${seg(planId)}/approve`);
197
+ }
198
+ async rejectPlan(planId, body) {
199
+ return this.request("POST", `/plans/${seg(planId)}/reject`, body);
200
+ }
178
201
  async listBuildRuns(productId, opts) {
179
202
  const params = new URLSearchParams();
180
203
  if (opts?.backlogItemId) params.set("backlogItemId", opts.backlogItemId);
@@ -183,9 +206,24 @@ var TaigaClient = class {
183
206
  const qs = params.toString();
184
207
  return this.request("GET", `/products/${seg(productId)}/builds${qs ? `?${qs}` : ""}`);
185
208
  }
209
+ /** The runs of every product the key can see, newest first; `active` narrows to the ones executing now. */
210
+ async listBuildRunActivity(opts) {
211
+ const params = new URLSearchParams();
212
+ if (opts?.active) params.set("active", "true");
213
+ if (opts?.limit) params.set("limit", String(opts.limit));
214
+ if (opts?.cursor) params.set("cursor", opts.cursor);
215
+ const qs = params.toString();
216
+ return this.request("GET", `/builds${qs ? `?${qs}` : ""}`);
217
+ }
186
218
  async getBuildRun(id) {
187
219
  return this.request("GET", `/builds/${seg(id)}`);
188
220
  }
221
+ async stopBuildRun(id) {
222
+ return this.request("POST", `/builds/${seg(id)}/stop`);
223
+ }
224
+ async resumeBuildRun(id) {
225
+ return this.request("POST", `/builds/${seg(id)}/resume`);
226
+ }
189
227
  async listBuildRunSteps(id) {
190
228
  return this.request("GET", `/builds/${seg(id)}/steps`);
191
229
  }
@@ -218,8 +256,10 @@ function readConfig() {
218
256
  //#endregion
219
257
  //#region src/tools/annotations.ts
220
258
  /**
221
- * Tool annotations shared by every read tool. Hints for the client, not a
222
- * control: the API enforces what a key may do.
259
+ * Tool annotations shared by the tools. Hints for the client, not a
260
+ * control: the API enforces what a key may do. A client that asks the
261
+ * person before a destructive call reads destructiveHint; the server-side
262
+ * approval gate (PRO-548) is separate work.
223
263
  */
224
264
  const READ_ONLY = {
225
265
  readOnlyHint: true,
@@ -227,6 +267,24 @@ const READ_ONLY = {
227
267
  idempotentHint: true,
228
268
  openWorldHint: false
229
269
  };
270
+ /** A write that adds or edits a row and starts nothing: a created initiative, a moved one. */
271
+ const WRITE = {
272
+ readOnlyHint: false,
273
+ destructiveHint: false,
274
+ idempotentHint: false,
275
+ openWorldHint: false
276
+ };
277
+ /**
278
+ * A write that starts or stops paid work, or opens the door to it: queueing
279
+ * an initiative, approving a plan, resuming or stopping a run. Each starts
280
+ * an agent that spends tokens and opens pull requests, or halts one mid-way.
281
+ */
282
+ const OPERATE = {
283
+ readOnlyHint: false,
284
+ destructiveHint: true,
285
+ idempotentHint: false,
286
+ openWorldHint: false
287
+ };
230
288
  //#endregion
231
289
  //#region src/tools/inputs.ts
232
290
  /** The inputs several tools share, so a product or a page is named the same way everywhere. */
@@ -466,6 +524,49 @@ const SETTLED_BACKLOG_STATUSES = ["done", "canceled"];
466
524
  function isSettledOrArchived(item) {
467
525
  return item.archivedAt != null || SETTLED_BACKLOG_STATUSES.includes(item.status);
468
526
  }
527
+ /**
528
+ * The columns a person can drop an initiative into by hand.
529
+ *
530
+ * `plan` is NOT one of them any more. Planning is the first thing the Build
531
+ * station does, not a place you put work, so the only way to start an
532
+ * initiative is to queue it and let its turn come -- which is also what makes
533
+ * "one at a time" true of planning as well as building, since the line holds
534
+ * one active entry per project and nothing else starts work. `build` is not
535
+ * droppable either: you cannot drop work INTO the station, only take it out.
536
+ * `done` and `canceled` ARE (PRO-1325): finishing work by hand, and reopening
537
+ * it, are moves on the board like any other.
538
+ */
539
+ const USER_DROP_STATES = [
540
+ "backlog",
541
+ "todo",
542
+ "queue",
543
+ "done",
544
+ "canceled"
545
+ ];
546
+ /**
547
+ * The moves that take an initiative off the line for good, and so close a
548
+ * pull request it is still waiting on (PRO-2476). Queue is not one of them:
549
+ * a pull request sent back to the queue waits its turn and is adopted
550
+ * (PRO-2468). Neither is Done: marked done by hand, the pull request may
551
+ * still be merged.
552
+ */
553
+ const PR_CLOSING_STATES = [
554
+ "backlog",
555
+ "todo",
556
+ "canceled"
557
+ ];
558
+ /**
559
+ * What `PUT /backlog/:id/state` accepts: the drops, plus `plan`.
560
+ *
561
+ * `plan` is not a column a person drops into any more, but it is still a state
562
+ * the endpoint takes -- it is how the Build station is told to start over
563
+ * ("Plan again"). One endpoint for both because they are the same write: the
564
+ * initiative's state, decided by a person.
565
+ */
566
+ const INITIATIVE_TRANSITION_STATES = [...USER_DROP_STATES, "plan"];
567
+ /** Longest rejection comment on a plan (PRO-1324): a paragraph of direction,
568
+ * small enough that the prompt section it becomes stays a section. */
569
+ const PLAN_REJECTION_COMMENT_MAX_CHARS = 2e3;
469
570
  const TURN_EVENT_TRIGGERS = [
470
571
  "defined",
471
572
  "plan_started",
@@ -1494,16 +1595,20 @@ function describeMiss(type, wanted, timelines, product) {
1494
1595
  * repository filter). Exactly one of the three; a remote linked to several
1495
1596
  * products is ambiguous and says so.
1496
1597
  */
1497
- async function resolveProductRef(client, ref, accepted) {
1598
+ async function resolveProductRef(client, ref, accepted, usage) {
1498
1599
  const refusal = (code, message) => ({ refusal: {
1499
1600
  code,
1500
1601
  message
1501
1602
  } });
1502
- if ([
1603
+ const given = [
1503
1604
  ref.productId,
1504
1605
  ref.slug,
1505
1606
  ref.remoteUrl
1506
- ].filter((v) => v !== void 0).length !== 1) return refusal("VALIDATION_FAILED", `Name the product exactly once: ${accepted.join(" or ")}. Two names could mean two products.`);
1607
+ ].filter((v) => v !== void 0).length;
1608
+ if (given !== 1) {
1609
+ const how = given === 0 ? usage ?? "taiga_list_products shows what exists." : "Two names could mean two products.";
1610
+ return refusal("VALIDATION_FAILED", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
1611
+ }
1507
1612
  if (ref.productId) try {
1508
1613
  return { product: (await client.getProduct(ref.productId)).data };
1509
1614
  } catch (error) {
@@ -1842,8 +1947,44 @@ const BACKLOG_SUMMARY_FIELDS = [
1842
1947
  "createdAt",
1843
1948
  "updatedAt"
1844
1949
  ];
1950
+ /**
1951
+ * What an initiative says about itself after a write: where it is on the
1952
+ * line, whose turn it is and what the line is waiting for. Not the brief,
1953
+ * the criteria or the technical context the caller already has.
1954
+ */
1955
+ const ITEM_OUTCOME_FIELDS = [
1956
+ "id",
1957
+ "productId",
1958
+ "title",
1959
+ "status",
1960
+ "state",
1961
+ "planningStatus",
1962
+ "needsHumanCause",
1963
+ "needsHumanDetail",
1964
+ "lifecycle"
1965
+ ];
1966
+ const itemId = z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items");
1967
+ const outcome = (item) => toolResult(pickFields([item], ITEM_OUTCOME_FIELDS)[0]);
1968
+ /**
1969
+ * Each tool is registered under the capability its route requires, so a
1970
+ * key sees exactly what it may call: reads on backlog:read, the create and
1971
+ * the board move on backlog:write, queueing on both backlog:write and
1972
+ * products:write (the state route checks both for a drop into queue), the
1973
+ * resume on products:write alone, as its route is (the item ID it takes
1974
+ * also comes back from a create or a queue), and the plan verdicts on
1975
+ * products:write together with backlog:read, because the plan ID they take
1976
+ * comes only from the plan read: a key that cannot read plans has nothing
1977
+ * to feed them.
1978
+ */
1845
1979
  function registerBacklogTools(server, client) {
1846
- if (!client.hasCapability("backlog:read")) return;
1980
+ const may = (capability) => client.hasCapability(capability);
1981
+ if (may("backlog:read")) registerBacklogReads(server, client);
1982
+ if (may("backlog:write")) registerBacklogWrites(server, client);
1983
+ if (may("backlog:write") && may("products:write")) registerQueue(server, client);
1984
+ if (may("products:write")) registerResume(server, client);
1985
+ if (may("backlog:read") && may("products:write")) registerPlanVerdicts(server, client);
1986
+ }
1987
+ function registerBacklogReads(server, client) {
1847
1988
  server.registerTool("taiga_list_backlog_items", {
1848
1989
  title: "List backlog items",
1849
1990
  description: "List a product's backlog items (initiatives): titles, category, status and where each one is on the line (backlog, todo, queue, plan, build, done). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
@@ -1883,6 +2024,95 @@ function registerBacklogTools(server, client) {
1883
2024
  }, (params) => runTool(async () => {
1884
2025
  return toolResult((await client.getBacklogItem(params.itemId)).data);
1885
2026
  }));
2027
+ server.registerTool("taiga_get_plan", {
2028
+ title: "Get the active plan",
2029
+ description: "Get an initiative's active plan with its steps: what each step changes, the files it writes and reads, its layer and dependencies. Read this before taiga_approve_plan or taiga_reject_plan, and to see what a run will do. NOT_FOUND when the initiative has no active plan yet (planningStatus says whether one is being generated).",
2030
+ inputSchema: { itemId },
2031
+ annotations: READ_ONLY
2032
+ }, (params) => runTool(async () => {
2033
+ return toolResult((await client.getActivePlan(params.itemId)).data);
2034
+ }));
2035
+ }
2036
+ function registerBacklogWrites(server, client) {
2037
+ server.registerTool("taiga_create_backlog_item", {
2038
+ title: "Create backlog item",
2039
+ description: "Create an initiative in the Backlog column of a product: a title and a brief in Markdown (the problem, what it looks like, what it covers and leaves alone), its category, priority and estimate. It starts nothing: the planner reads it only once it is queued (taiga_queue_backlog_item) or moved to Plan (taiga_move_backlog_item). Returns the new item with its ID.",
2040
+ inputSchema: {
2041
+ productId,
2042
+ title: z.string().min(1).max(500).describe("The initiative title, as it appears on the board"),
2043
+ description: z.string().min(1).max(2e4).describe("The brief in Markdown: the problem, what it looks like, what it covers and leaves alone"),
2044
+ category: z.enum([
2045
+ "feature",
2046
+ "bug",
2047
+ "tech_debt",
2048
+ "improvement"
2049
+ ]).optional().describe("What kind of work (default feature)"),
2050
+ priority: z.number().int().min(1).optional().describe("1 is the most urgent (default 3)"),
2051
+ estimate: z.number().int().min(0).optional().describe("Story points, 0 for unestimated (default 0)")
2052
+ },
2053
+ annotations: WRITE
2054
+ }, (params) => runTool(async () => {
2055
+ const { productId: id, ...body } = params;
2056
+ return outcome((await client.createBacklogItem(id, body)).data);
2057
+ }));
2058
+ server.registerTool("taiga_move_backlog_item", {
2059
+ title: "Move backlog item on the board",
2060
+ description: `Move an initiative to a column, or order it within one: backlog and todo are the idle columns; queue puts it in the line (taiga_queue_backlog_item says more); plan starts planning now; done and canceled take it off the line. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged). The initiative state machine refuses a move that is not allowed (CONFLICT: build cannot be dropped into, an initiative being planned cannot leave plan). A move into queue or plan starts paid work and needs products:write on the key; a key without it is refused here. Ask the person before a move that starts work (queue, plan) or closes a pull request (${PR_CLOSING_STATES.join(", ")} from build).`,
2061
+ inputSchema: {
2062
+ itemId,
2063
+ state: z.enum(INITIATIVE_TRANSITION_STATES).describe("The column to move into"),
2064
+ afterId: z.string().uuid().nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order")
2065
+ },
2066
+ annotations: OPERATE
2067
+ }, (params) => runTool(async () => {
2068
+ const { itemId: id, ...body } = params;
2069
+ if (LINE_STARTING_STATES.has(body.state) && !client.hasCapability("products:write")) return toolError("FORBIDDEN", `A move into ${body.state} starts agent work and needs products:write, which this key lacks.`);
2070
+ return outcome((await client.transitionInitiative(id, body)).data);
2071
+ }));
2072
+ }
2073
+ /** The columns a drop into starts paid work, which the state route gates on products:write. */
2074
+ const LINE_STARTING_STATES = /* @__PURE__ */ new Set(["queue", "plan"]);
2075
+ function registerQueue(server, client) {
2076
+ server.registerTool("taiga_queue_backlog_item", {
2077
+ title: "Queue backlog item",
2078
+ description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then (when the plan is approved, or at once if auto-build is on) a coding run that pushes commits and opens a pull request in the product repository. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state.",
2079
+ inputSchema: {
2080
+ itemId,
2081
+ afterId: z.string().uuid().nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is")
2082
+ },
2083
+ annotations: OPERATE
2084
+ }, (params) => runTool(async () => {
2085
+ const { itemId: id, ...placement } = params;
2086
+ return outcome((await client.transitionInitiative(id, {
2087
+ state: "queue",
2088
+ ...placement
2089
+ })).data);
2090
+ }));
2091
+ }
2092
+ function registerResume(server, client) {
2093
+ server.registerTool("taiga_resume_backlog_item", {
2094
+ title: "Resume backlog item",
2095
+ description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step, a refused build start is cleared and retried. Does whatever the named cause needs, so prefer it to taiga_resume_build when the initiative is flagged. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
2096
+ inputSchema: { itemId },
2097
+ annotations: OPERATE
2098
+ }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
2099
+ }
2100
+ function registerPlanVerdicts(server, client) {
2101
+ server.registerTool("taiga_approve_plan", {
2102
+ title: "Approve a plan",
2103
+ description: "Approve an initiative's plan: its build starts now, as the approver, the same start the Start run button makes. Read the plan first with taiga_get_plan and name its ID; an approval of a plan that was replaced since is refused (CONFLICT), as is one while a new plan is being generated or a build is already running. Starts a coding run that pushes commits and opens a pull request; ask the person first.",
2104
+ inputSchema: { planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan") },
2105
+ annotations: OPERATE
2106
+ }, (params) => runTool(async () => outcome((await client.approvePlan(params.planId)).data)));
2107
+ server.registerTool("taiga_reject_plan", {
2108
+ title: "Reject a plan",
2109
+ description: "Send an initiative's plan back with direction: a new plan is generated with the comment as reviewer feedback and the rejected plan as its starting point, which supersedes it when the new one lands. Read the plan first with taiga_get_plan and name its ID. Starts a planner run; ask the person first.",
2110
+ inputSchema: {
2111
+ planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan"),
2112
+ comment: z.string().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).describe("What the next plan should do differently: a paragraph of direction")
2113
+ },
2114
+ annotations: OPERATE
2115
+ }, (params) => runTool(async () => outcome((await client.rejectPlan(params.planId, { comment: params.comment })).data)));
1886
2116
  }
1887
2117
  //#endregion
1888
2118
  //#region src/tools/builds.ts
@@ -1890,6 +2120,7 @@ function registerBacklogTools(server, client) {
1890
2120
  const BUILD_SUMMARY_FIELDS = [
1891
2121
  "id",
1892
2122
  "runNumber",
2123
+ "productId",
1893
2124
  "backlogItemId",
1894
2125
  "targetTitle",
1895
2126
  "planId",
@@ -1906,29 +2137,79 @@ const BUILD_SUMMARY_FIELDS = [
1906
2137
  "prState",
1907
2138
  "prDraft",
1908
2139
  "prMerged",
2140
+ "actions",
1909
2141
  "createdAt",
1910
2142
  "startedAt",
1911
2143
  "completedAt"
1912
2144
  ];
2145
+ /** What a run says about itself after a stop or a resume: its state and what can be done next. */
2146
+ const RUN_OUTCOME_FIELDS = [
2147
+ "id",
2148
+ "runNumber",
2149
+ "backlogItemId",
2150
+ "status",
2151
+ "failedReason",
2152
+ "completedSteps",
2153
+ "totalSteps",
2154
+ "prUrl",
2155
+ "actions"
2156
+ ];
2157
+ /** How many products the org-wide list names; past this a row keeps its productId only. */
2158
+ const NAMED_PRODUCTS = 100;
2159
+ const buildId = z.string().uuid().describe("Build run ID -- get from taiga_list_builds");
2160
+ /**
2161
+ * The runs of every product the key can see, each row naming its product.
2162
+ * The route carries product ids only; one products read names them, and a
2163
+ * failure of that read leaves the rows unnamed rather than failing the list.
2164
+ */
2165
+ async function activityRows(client, opts) {
2166
+ const [runs, products] = await Promise.all([client.listBuildRunActivity(opts), client.listProducts({ limit: NAMED_PRODUCTS }).catch((error) => {
2167
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
2168
+ throw error;
2169
+ })]);
2170
+ const nameById = /* @__PURE__ */ new Map();
2171
+ for (const product of products?.data ?? []) nameById.set(String(product.id), {
2172
+ name: product.name,
2173
+ slug: product.slug
2174
+ });
2175
+ const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map((row) => {
2176
+ const product = nameById.get(String(row.productId));
2177
+ return product ? {
2178
+ ...row,
2179
+ product
2180
+ } : row;
2181
+ });
2182
+ const note = !rows.some((row) => !("product" in row)) ? null : !products ? "The products could not be read, so rows carry productId only; taiga_get_product names one." : products.nextCursor !== null ? `Only the first ${NAMED_PRODUCTS} products could be named; a row without product carries productId only.` : "A row without product belongs to a product the key cannot list (deleted since, or not visible to it); it carries productId only.";
2183
+ return {
2184
+ ...page(rows, runs.nextCursor),
2185
+ ...note ? { note } : {}
2186
+ };
2187
+ }
1913
2188
  function registerBuildTools(server, client) {
1914
2189
  if (!client.hasCapability("products:read")) return;
1915
2190
  server.registerTool("taiga_list_builds", {
1916
2191
  title: "List build runs",
1917
- description: "List the coding agent's runs for a product, newest first: status, branch, pull request and step progress. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. Filter by backlog item to follow a single initiative.",
2192
+ description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each (stop is taiga_stop_build; resume and retry_from_failed_step are both taiga_resume_build, the same route; start_over is only in the web UI for now). With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
1918
2193
  inputSchema: {
1919
- productId,
1920
- backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items"),
2194
+ productId: productId.optional(),
2195
+ backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
2196
+ active: z.boolean().optional().describe("Only runs the agent is executing now (cloning through reviewing; a queued pending run is not yet executing and is left out); for the org-wide list"),
1921
2197
  ...pagination
1922
2198
  },
1923
2199
  annotations: READ_ONLY
1924
2200
  }, (params) => runTool(async () => {
1925
- const result = await client.listBuildRuns(params.productId, params);
1926
- return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
2201
+ if (!params.productId && params.backlogItemId) return toolError("VALIDATION_FAILED", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
2202
+ if (params.productId && params.active) return toolError("VALIDATION_FAILED", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
2203
+ if (params.productId) {
2204
+ const result = await client.listBuildRuns(params.productId, params);
2205
+ return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
2206
+ }
2207
+ return toolResult(await activityRows(client, params));
1927
2208
  }));
1928
2209
  server.registerTool("taiga_get_build", {
1929
2210
  title: "Get build run",
1930
- description: "Get one run in full with its steps: what each step was meant to change, what it did change (commit, files), its status and any error. Use this to pick up where the coding agent stopped, or to understand the pull request it opened.",
1931
- inputSchema: { buildId: z.string().uuid().describe("Build run ID -- get from taiga_list_builds") },
2211
+ description: "Get one run in full with its steps: what each step was meant to change, what it did change (commit, files), its status and any error. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions list says what it accepts: stop (taiga_stop_build), resume or retry_from_failed_step (both taiga_resume_build), start_over (web UI only for now).",
2212
+ inputSchema: { buildId },
1932
2213
  annotations: READ_ONLY
1933
2214
  }, (params) => runTool(async () => {
1934
2215
  const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
@@ -1937,6 +2218,23 @@ function registerBuildTools(server, client) {
1937
2218
  steps: steps.data
1938
2219
  });
1939
2220
  }));
2221
+ if (!client.hasCapability("products:write")) return;
2222
+ server.registerTool("taiga_stop_build", {
2223
+ title: "Stop a build run",
2224
+ description: "Stop a run the coding agent is executing: the agent halts, its branch and any draft pull request stay, and the run can be resumed later with taiga_resume_build. Only a run whose actions include stop (taiga_list_builds shows them); the API refuses the rest with CONFLICT. Ask the person before calling this: it interrupts work in progress.",
2225
+ inputSchema: { buildId },
2226
+ annotations: OPERATE
2227
+ }, (params) => runTool(async () => {
2228
+ return toolResult(pickFields([(await client.stopBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
2229
+ }));
2230
+ server.registerTool("taiga_resume_build", {
2231
+ title: "Resume a build run",
2232
+ description: "Resume a stopped or failed run from its first incomplete step: the agent starts again on the same branch, which spends tokens and may push commits and open a pull request. This is the route behind both the resume and the retry_from_failed_step actions: the API accepts a run whose actions include one of them and refuses the rest with CONFLICT. start_over (a new run) is web-only for now. For an initiative the line stopped on, taiga_resume_backlog_item does whatever its cause needs instead. Ask the person before calling this.",
2233
+ inputSchema: { buildId },
2234
+ annotations: OPERATE
2235
+ }, (params) => runTool(async () => {
2236
+ return toolResult(pickFields([(await client.resumeBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
2237
+ }));
1940
2238
  }
1941
2239
  //#endregion
1942
2240
  //#region src/tools/context.ts
@@ -2300,16 +2598,16 @@ function registerPrompts(server, client) {
2300
2598
  title: "Start a session on a product",
2301
2599
  description: "Open a coding session with everything Taiga knows about the product: its factory instructions, specification and architecture, the policies that apply, the open backlog and the latest runs. Name the product by the git remote URL of the checkout, its slug or its ID.",
2302
2600
  argsSchema: {
2303
- remoteUrl: remoteUrl.optional(),
2304
2601
  slug: slug.optional(),
2305
- productId: productId.optional()
2602
+ productId: productId.optional(),
2603
+ remoteUrl: remoteUrl.optional()
2306
2604
  }
2307
2605
  }, async (args) => {
2308
2606
  const resolved = await resolveProductRef(client, args, [
2309
- "remoteUrl",
2310
2607
  "slug",
2311
- "productId"
2312
- ]);
2608
+ "productId",
2609
+ "remoteUrl"
2610
+ ], "Pass the slug as the first argument (/taiga_start_session <slug>, the slug from the Taiga URL); taiga_list_products lists the slugs.");
2313
2611
  if ("refusal" in resolved) refuse(resolved.refusal);
2314
2612
  return message([
2315
2613
  "You are working on a product managed in Taiga. The fenced JSON below is its context: the product and",
@@ -2567,7 +2865,16 @@ const SERVER_INSTRUCTIONS = [
2567
2865
  "",
2568
2866
  "**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
2569
2867
  "The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
2570
- "for them: runs, steps, commits and pull requests.",
2868
+ "for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
2869
+ "every product the key can see, and active: true the ones executing now.",
2870
+ "",
2871
+ "**Operating the line** (taiga_create_backlog_item / taiga_queue_backlog_item / taiga_move_backlog_item /",
2872
+ "taiga_resume_backlog_item / taiga_get_plan / taiga_approve_plan / taiga_reject_plan / taiga_stop_build /",
2873
+ "taiga_resume_build): create an initiative, put it in the queue the coding agent works, move it on the",
2874
+ "board, read and approve or reject its plan, stop or resume a run. Queueing, approving and resuming start",
2875
+ "paid agent work that pushes commits and opens pull requests; stopping interrupts it. Tell the person what",
2876
+ "you are about to start or stop and wait for their yes before calling these; a client that prompts on",
2877
+ "destructive tools does that for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
2571
2878
  "",
2572
2879
  "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
2573
2880
  "provenance fields you can read past: `source` says where the text originally came from (template:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.9.0-rc.403",
3
+ "version": "0.10.0-rc.405",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/client.ts CHANGED
@@ -295,6 +295,44 @@ export class TaigaClient {
295
295
  return this.request<{ data: unknown }>('GET', `/backlog/${seg(itemId)}`);
296
296
  }
297
297
 
298
+ async createBacklogItem(
299
+ productId: string,
300
+ body: {
301
+ title: string;
302
+ description: string;
303
+ category?: string | undefined;
304
+ priority?: number | undefined;
305
+ estimate?: number | undefined;
306
+ }
307
+ ) {
308
+ // The route's body schema names the product too, as the web's
309
+ // CreateBacklogItemInput does (the live run found it; the handler then
310
+ // takes the one from the path). Dropping it is an API contract change.
311
+ return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/backlog`, { ...body, productId });
312
+ }
313
+
314
+ /** A person's move on the line board: the column dropped into, and what to follow there. */
315
+ async transitionInitiative(itemId: string, body: { state: string; afterId?: string | null | undefined }) {
316
+ return this.request<{ data: unknown }>('PUT', `/backlog/${seg(itemId)}/state`, body);
317
+ }
318
+
319
+ /** Answer the needs-human flag: plan again, resume the run, or clear a refused start. */
320
+ async resumeInitiative(itemId: string) {
321
+ return this.request<{ data: unknown }>('POST', `/backlog/${seg(itemId)}/resume`);
322
+ }
323
+
324
+ async getActivePlan(itemId: string) {
325
+ return this.request<{ data: unknown }>('GET', `/backlog/${seg(itemId)}/plans/active`);
326
+ }
327
+
328
+ async approvePlan(planId: string) {
329
+ return this.request<{ data: unknown }>('POST', `/plans/${seg(planId)}/approve`);
330
+ }
331
+
332
+ async rejectPlan(planId: string, body: { comment: string }) {
333
+ return this.request<{ data: unknown }>('POST', `/plans/${seg(planId)}/reject`, body);
334
+ }
335
+
298
336
  // ---------------------------------------------------------------------------
299
337
  // Build runs
300
338
  // ---------------------------------------------------------------------------
@@ -314,10 +352,33 @@ export class TaigaClient {
314
352
  );
315
353
  }
316
354
 
355
+ /** The runs of every product the key can see, newest first; `active` narrows to the ones executing now. */
356
+ async listBuildRunActivity(opts?: {
357
+ active?: boolean | undefined;
358
+ limit?: number | undefined;
359
+ cursor?: string | undefined;
360
+ }) {
361
+ const params = new URLSearchParams();
362
+ // The route reads the literal strings 'true' and 'false'.
363
+ if (opts?.active) params.set('active', 'true');
364
+ if (opts?.limit) params.set('limit', String(opts.limit));
365
+ if (opts?.cursor) params.set('cursor', opts.cursor);
366
+ const qs = params.toString();
367
+ return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/builds${qs ? `?${qs}` : ''}`);
368
+ }
369
+
317
370
  async getBuildRun(id: string) {
318
371
  return this.request<{ data: unknown }>('GET', `/builds/${seg(id)}`);
319
372
  }
320
373
 
374
+ async stopBuildRun(id: string) {
375
+ return this.request<{ data: unknown }>('POST', `/builds/${seg(id)}/stop`);
376
+ }
377
+
378
+ async resumeBuildRun(id: string) {
379
+ return this.request<{ data: unknown }>('POST', `/builds/${seg(id)}/resume`);
380
+ }
381
+
321
382
  async listBuildRunSteps(id: string) {
322
383
  return this.request<{ data: unknown[] }>('GET', `/builds/${seg(id)}/steps`);
323
384
  }