@taiga-ai/mcp-server 0.14.0-dev.428 → 0.16.0-dev.430

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.
@@ -0,0 +1,266 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
+ import { z } from 'zod';
3
+ import type { TaigaClient } from '../client.js';
4
+ import { OPERATE, OPERATE_SETTING, READ_ONLY } from './annotations.js';
5
+ import { runTool, toolError, toolResult } from './errors.js';
6
+ import { pagination, productId } from './inputs.js';
7
+ import { page, pickFields } from './summarize.js';
8
+
9
+ /**
10
+ * A product's repository (docs.tai.ga, Administration: Integrations and
11
+ * environments, Source control). A linked repository is the prerequisite for
12
+ * most of what Taiga does: planning reads the code, runs work on a branch of
13
+ * it, sweeps scan it and the as-built documents are derived from it. GitHub is
14
+ * connected once for the organization, in the web; linking one of the
15
+ * repositories it can reach to a product is a separate act, per product.
16
+ */
17
+
18
+ type Row = Record<string, unknown>;
19
+
20
+ const LINK_FIELDS = [
21
+ 'id',
22
+ 'provider',
23
+ 'repositoryName',
24
+ 'repositoryUrl',
25
+ 'repositoryDefaultBranch',
26
+ 'baseBranch',
27
+ 'effectiveBranch',
28
+ 'label',
29
+ 'buildable',
30
+ 'isManaged',
31
+ ] as const;
32
+
33
+ const INSTALLATION_FIELDS = [
34
+ 'id',
35
+ 'accountLogin',
36
+ 'accountType',
37
+ 'repositorySelection',
38
+ 'suspendedAt',
39
+ 'lastSyncError',
40
+ ] as const;
41
+
42
+ const GITHUB_REPOSITORY_FIELDS = [
43
+ 'id',
44
+ 'fullName',
45
+ 'private',
46
+ 'archived',
47
+ 'defaultBranch',
48
+ 'description',
49
+ 'language',
50
+ ] as const;
51
+
52
+ const repositoryId = z
53
+ .string()
54
+ .uuid()
55
+ .describe("The product's repository link ID -- taiga_list_product_repositories gives it");
56
+
57
+ export function registerRepositoryTools(server: McpServer, client: TaigaClient) {
58
+ if (client.hasCapability('products:read')) {
59
+ server.registerTool(
60
+ 'taiga_list_product_repositories',
61
+ {
62
+ title: "List a product's repositories",
63
+ description:
64
+ 'The repositories linked to a product, each with the branch Taiga works on (effectiveBranch: the ' +
65
+ "chosen branch, or the repository's default) and whether Taiga can plan and build in it (buildable).",
66
+ inputSchema: z.object({ productId }),
67
+ annotations: READ_ONLY,
68
+ },
69
+ (params) =>
70
+ runTool(async () => {
71
+ const result = await client.listProductRepositories(params.productId);
72
+ return toolResult(pickFields(result.data as Row[], LINK_FIELDS));
73
+ })
74
+ );
75
+
76
+ server.registerTool(
77
+ 'taiga_list_repository_branches',
78
+ {
79
+ title: "List a repository's branches",
80
+ description:
81
+ "The branches of a product's linked repository, to choose the one Taiga branches from and opens pull " +
82
+ 'requests against (taiga_set_repository_branch); each says whether it is protected. truncated: true ' +
83
+ 'means the repository has more branches than this answer lists, and the rest were left out.',
84
+ inputSchema: z.object({ productId, repositoryId }),
85
+ annotations: READ_ONLY,
86
+ },
87
+ (params) =>
88
+ runTool(async () => {
89
+ const result = await client.listRepositoryBranches(params.productId, params.repositoryId);
90
+ return toolResult({ branches: result.data, truncated: result.truncated });
91
+ })
92
+ );
93
+ }
94
+
95
+ if (client.hasCapability('vcs:read')) {
96
+ server.registerTool(
97
+ 'taiga_list_linkable_repositories',
98
+ {
99
+ title: 'Find a repository to link',
100
+ description:
101
+ "The repositories the organization's GitHub connection can reach, to link one to a product " +
102
+ '(taiga_link_repository takes its id). Searches by name with query; without installationId it reads ' +
103
+ 'every connected GitHub account. No connection means GitHub is not connected yet: that is done once, ' +
104
+ 'in the web, by someone who can install the app.',
105
+ inputSchema: z.object({
106
+ query: z.string().trim().min(1).max(256).optional().describe('Part of the repository name'),
107
+ installationId: z.string().uuid().optional().describe('One GitHub account -- this tool lists them'),
108
+ ...pagination,
109
+ }),
110
+ annotations: READ_ONLY,
111
+ },
112
+ (params) =>
113
+ runTool(async () => {
114
+ const installations = (await client.listGithubInstallations()).data as Row[];
115
+ const accounts = pickFields(installations, INSTALLATION_FIELDS);
116
+ if (installations.length === 0) {
117
+ return toolError(
118
+ 'NOT_CONNECTED',
119
+ 'GitHub is not connected for this organization. It is connected once, in the web; then link.'
120
+ );
121
+ }
122
+ const chosen = params.installationId
123
+ ? installations.filter((i) => i.id === params.installationId)
124
+ : installations;
125
+ if (chosen.length === 0) {
126
+ return toolError('NOT_FOUND', 'No such GitHub connection; these are the connected accounts.', { accounts });
127
+ }
128
+ // A cursor belongs to one account's pages, and a search answers one
129
+ // ranked page: either way a cursor would quietly start over.
130
+ if (params.cursor !== undefined && params.query !== undefined) {
131
+ return toolError('VALIDATION_FAILED', 'A search answers one page; narrow the query instead of paging it.');
132
+ }
133
+ if (chosen.length > 1 && params.cursor !== undefined) {
134
+ return toolError('VALIDATION_FAILED', 'A cursor pages one account: name its installationId.', { accounts });
135
+ }
136
+ // `limit` is the most rows the answer carries, however many accounts
137
+ // are read: a search answers its ranked matches whatever the page
138
+ // size, and several accounts add up.
139
+ const max = params.limit ?? 25;
140
+ const rows = (data: unknown[]) => pickFields(data as Row[], GITHUB_REPOSITORY_FIELDS);
141
+ if (chosen.length === 1) {
142
+ const result = await client.listInstallationRepositories(String(chosen[0]!.id), {
143
+ q: params.query,
144
+ limit: max,
145
+ cursor: params.cursor,
146
+ });
147
+ const found = rows(result.data);
148
+ return toolResult({
149
+ accounts,
150
+ repositories: page(found.slice(0, max), result.nextCursor),
151
+ ...(found.length > max
152
+ ? { more: 'More matched than the limit; narrow the query or raise the limit.' }
153
+ : {}),
154
+ });
155
+ }
156
+ // Several accounts are read once each, as the web's picker reads them;
157
+ // a cursor belongs to one account, so the page has none.
158
+ const pages = await Promise.all(
159
+ chosen.map((i) => client.listInstallationRepositories(String(i.id), { q: params.query, limit: max }))
160
+ );
161
+ const found = pages.flatMap((p) => rows(p.data));
162
+ const cut = found.length > max || pages.some((p) => p.nextCursor);
163
+ return toolResult({
164
+ accounts,
165
+ repositories: page(found.slice(0, max), null),
166
+ ...(cut
167
+ ? { more: 'More than the limit across accounts: name an installationId, or narrow the query.' }
168
+ : {}),
169
+ });
170
+ })
171
+ );
172
+ }
173
+
174
+ if (!client.hasCapability('vcs:manage')) return;
175
+
176
+ server.registerTool(
177
+ 'taiga_link_repository',
178
+ {
179
+ title: 'Link a repository to a product',
180
+ description:
181
+ 'Link a repository the GitHub connection can reach (taiga_list_linkable_repositories gives its id) to a ' +
182
+ 'product, or with replacesRepositoryId swap the one linked now for it in one step (naming the same ' +
183
+ 'repository there changes nothing; linking one already linked without it is refused). A link Taiga ' +
184
+ 'manages (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, ' +
185
+ 'sweeps scan it, and its deployment history is imported in the background. Ask the person first.',
186
+ inputSchema: z.object({
187
+ productId,
188
+ githubRepositoryId: z
189
+ .string()
190
+ .uuid()
191
+ .describe('The repository id -- taiga_list_linkable_repositories gives it'),
192
+ replacesRepositoryId: repositoryId.optional().describe('The link this one replaces'),
193
+ }),
194
+ annotations: OPERATE,
195
+ },
196
+ (params) =>
197
+ runTool(async () => {
198
+ const linked = await client.linkRepository(params.productId, {
199
+ githubRepositoryId: params.githubRepositoryId,
200
+ ...(params.replacesRepositoryId ? { replacesRepositoryId: params.replacesRepositoryId } : {}),
201
+ });
202
+ return toolResult(pickFields([linked.data as Row], LINK_FIELDS)[0]);
203
+ })
204
+ );
205
+
206
+ server.registerTool(
207
+ 'taiga_set_repository_branch',
208
+ {
209
+ title: 'Choose the branch Taiga works on',
210
+ description:
211
+ "Choose the branch of a product's repository that Taiga branches from and opens its pull requests " +
212
+ "against (taiga_list_repository_branches lists them), or null to follow the repository's default " +
213
+ 'again. A protected branch is not refused, but Taiga may not be able to merge into it: the answer says ' +
214
+ 'when it is protected. Optionally a label for the link. Ask the person first.',
215
+ inputSchema: z.object({
216
+ productId,
217
+ repositoryId,
218
+ baseBranch: z
219
+ .string()
220
+ .trim()
221
+ .min(1)
222
+ .max(255)
223
+ .nullable()
224
+ .optional()
225
+ .describe('The branch; null for the default'),
226
+ label: z.string().trim().min(1).max(100).nullable().optional().describe('A label for the link; null clears it'),
227
+ }),
228
+ annotations: OPERATE_SETTING,
229
+ },
230
+ (params) =>
231
+ runTool(async () => {
232
+ if (params.baseBranch === undefined && params.label === undefined) {
233
+ return toolError('VALIDATION_FAILED', 'Set baseBranch, label or both.');
234
+ }
235
+ const updated = (
236
+ await client.updateRepository(params.productId, params.repositoryId, {
237
+ ...(params.baseBranch !== undefined ? { baseBranch: params.baseBranch } : {}),
238
+ ...(params.label !== undefined ? { label: params.label } : {}),
239
+ })
240
+ ).data as Row;
241
+ return toolResult({
242
+ ...pickFields([updated], LINK_FIELDS)[0],
243
+ baseBranchProtection: updated.baseBranchProtection ?? null,
244
+ });
245
+ })
246
+ );
247
+
248
+ server.registerTool(
249
+ 'taiga_unlink_repository',
250
+ {
251
+ title: 'Unlink a repository from a product',
252
+ description:
253
+ 'Unlink a repository from a product. Without one, the product can still have documents and initiatives ' +
254
+ 'but no changes: nothing is planned against code, built or swept. The repository itself is untouched, ' +
255
+ 'and a repository Taiga created for the product cannot be unlinked. ' +
256
+ 'To swap it for another, use taiga_link_repository with replacesRepositoryId instead. Ask the person first.',
257
+ inputSchema: z.object({ productId, repositoryId }),
258
+ annotations: OPERATE,
259
+ },
260
+ (params) =>
261
+ runTool(async () => {
262
+ await client.unlinkRepository(params.productId, params.repositoryId);
263
+ return toolResult({ repositoryId: params.repositoryId, unlinked: true });
264
+ })
265
+ );
266
+ }
@@ -1,8 +1,12 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { ErrorCode, type ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
1
+ import {
2
+ type McpServer,
3
+ ProtocolError,
4
+ ProtocolErrorCode,
5
+ type ReadResourceResult,
6
+ ResourceTemplate,
7
+ } from '@modelcontextprotocol/server';
4
8
  import { TaigaApiError, type TaigaClient } from '../client.js';
5
- import { RequestError, apiFailureText, isMissOrRefusal } from './errors.js';
9
+ import { apiFailureText, isMissOrRefusal } from './errors.js';
6
10
  import {
7
11
  FACTORY_DOCUMENT_TYPES,
8
12
  PRODUCT_DOCUMENT_ORIGINS,
@@ -38,7 +42,7 @@ function contents(uri: string, doc: Record<string, unknown>): ReadResourceResult
38
42
  }
39
43
 
40
44
  function invalid(message: string): never {
41
- throw new RequestError(ErrorCode.InvalidParams, message);
45
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, message);
42
46
  }
43
47
 
44
48
  function requireOneOf<T extends string>(what: string, value: string, allowed: readonly T[]): T {
@@ -0,0 +1,342 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
+ import { DEPLOYMENT_NOTES_MAX_LENGTH, REALITY_DERIVED_DOCUMENT_TYPES } from '@taiga/types';
3
+ import { z } from 'zod';
4
+ import type { TaigaClient } from '../client.js';
5
+ import { OPERATE, OPERATE_SETTING, READ_ONLY, WRITE } from './annotations.js';
6
+ import { isImportedProduct, type ProductSettingsView } from './catalogue.js';
7
+ import { runTool, toolError, toolResult } from './errors.js';
8
+ import { productId } from './inputs.js';
9
+ import { pickFields } from './summarize.js';
10
+
11
+ /**
12
+ * Setting a product up (docs.tai.ga, Start: Your first product, Learn: Import
13
+ * an existing codebase, Organizations, factories and products): factories to
14
+ * put products in, a product started from scratch or from an imported
15
+ * codebase, what Taiga builds for it, the import, and finishing or reopening
16
+ * Discovery. Who may reach a factory (its members) and deleting a factory or
17
+ * a product stay in the web.
18
+ */
19
+
20
+ type Row = Record<string, unknown>;
21
+
22
+ const PRODUCT_FIELDS = [
23
+ 'id',
24
+ 'name',
25
+ 'slug',
26
+ 'status',
27
+ 'factoryId',
28
+ 'ownsInfrastructure',
29
+ 'ownsDeployment',
30
+ 'deploymentNotes',
31
+ 'emitDesignStandards',
32
+ 'settings',
33
+ ] as const;
34
+
35
+ const FACTORY_FIELDS = ['id', 'name', 'slug', 'isDefault'] as const;
36
+
37
+ const factoryId = z.string().uuid().describe('Factory ID -- taiga_list_factories gives it');
38
+
39
+ const buildScope = {
40
+ ownsInfrastructure: z
41
+ .boolean()
42
+ .nullable()
43
+ .optional()
44
+ .describe('Whether Taiga writes the infrastructure code; null follows the organization'),
45
+ ownsDeployment: z
46
+ .boolean()
47
+ .nullable()
48
+ .optional()
49
+ .describe('Whether Taiga writes the CI/CD pipelines; null follows the organization'),
50
+ };
51
+
52
+ const deploymentNotes = z
53
+ .string()
54
+ .trim()
55
+ .min(1)
56
+ .max(DEPLOYMENT_NOTES_MAX_LENGTH)
57
+ .describe('When a half is off: a sentence on how it is done today, which Taiga designs and plans against');
58
+
59
+ /** A product row the model can act on, saying how it started as the other product tools say it. */
60
+ function productRow(product: Row): Row {
61
+ const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0]!;
62
+ return { ...rest, projectType: isImportedProduct(product as ProductSettingsView) ? 'brownfield' : 'greenfield' };
63
+ }
64
+
65
+ /**
66
+ * The sentence is for a half Taiga does not build; the web asks for it only
67
+ * then. When both halves are decided on here, a sentence has nothing to say.
68
+ */
69
+ function notesWithNothingToDescribe(params: {
70
+ ownsInfrastructure?: boolean | null | undefined;
71
+ ownsDeployment?: boolean | null | undefined;
72
+ deploymentNotes?: string | null | undefined;
73
+ }) {
74
+ return typeof params.deploymentNotes === 'string' &&
75
+ params.ownsInfrastructure === true &&
76
+ params.ownsDeployment === true
77
+ ? toolError(
78
+ 'VALIDATION_FAILED',
79
+ 'deploymentNotes describes a half Taiga does not build; both are on, so leave it out.'
80
+ )
81
+ : null;
82
+ }
83
+
84
+ export function registerSetupTools(server: McpServer, client: TaigaClient) {
85
+ if (client.hasCapability('factories:write')) {
86
+ server.registerTool(
87
+ 'taiga_create_factory',
88
+ {
89
+ title: 'Create a factory',
90
+ description:
91
+ 'Create a factory: a group of products that share a knowledge base, one instructions document and ' +
92
+ 'their members, so products under it work from the same context. Members are added in the web. Then put ' +
93
+ 'products in it with taiga_create_product, or move one there with taiga_move_product. Owners and admins ' +
94
+ 'create factories.',
95
+ inputSchema: z.object({ name: z.string().trim().min(1).max(255).describe('The factory name') }),
96
+ annotations: WRITE,
97
+ },
98
+ (params) =>
99
+ runTool(async () =>
100
+ toolResult(pickFields([(await client.createFactory({ name: params.name })).data as Row], FACTORY_FIELDS)[0])
101
+ )
102
+ );
103
+
104
+ server.registerTool(
105
+ 'taiga_rename_factory',
106
+ {
107
+ title: 'Rename a factory',
108
+ description:
109
+ 'Rename a factory. Its products, members, knowledge and instructions stay as they are, but its slug ' +
110
+ 'follows the new name, so links to the factory change. Ask the person first.',
111
+ inputSchema: z.object({ factoryId, name: z.string().trim().min(1).max(255).describe('The new name') }),
112
+ annotations: OPERATE_SETTING,
113
+ },
114
+ (params) =>
115
+ runTool(async () =>
116
+ toolResult(
117
+ pickFields(
118
+ [(await client.updateFactory(params.factoryId, { name: params.name })).data as Row],
119
+ FACTORY_FIELDS
120
+ )[0]
121
+ )
122
+ )
123
+ );
124
+
125
+ server.registerTool(
126
+ 'taiga_preview_product_move',
127
+ {
128
+ title: 'Preview moving a product to another factory',
129
+ description:
130
+ 'Who gains and who loses access if a product moves to another factory: people reach a product through ' +
131
+ "its factory, so the source factory's members lose it and the target's gain it. People on both, the " +
132
+ "product's own members, and owners and admins keep it, and are named with why. Read this, and show it " +
133
+ 'to the person, before taiga_move_product.',
134
+ inputSchema: z.object({ productId, factoryId: factoryId.describe('The factory to move it to') }),
135
+ annotations: READ_ONLY,
136
+ },
137
+ (params) =>
138
+ runTool(async () => toolResult((await client.previewProductMove(params.productId, params.factoryId)).data))
139
+ );
140
+
141
+ server.registerTool(
142
+ 'taiga_move_product',
143
+ {
144
+ title: 'Move a product to another factory',
145
+ description:
146
+ 'Move a product to another factory. Access follows the factory, so this changes who can reach the ' +
147
+ "product (taiga_preview_product_move says who); the product's own knowledge files are re-filed onto " +
148
+ 'the new factory, and factory knowledge and instructions stay with the factory that owns them. Refused ' +
149
+ '(CONFLICT) while a plan or build is in flight: stopLine: true stops the line first and then moves, which ' +
150
+ 'interrupts that work. Ask the person first.',
151
+ inputSchema: z.object({
152
+ productId,
153
+ factoryId: factoryId.describe('The factory to move it to'),
154
+ stopLine: z
155
+ .boolean()
156
+ .optional()
157
+ .describe('Stop the line first, interrupting the plan or build in flight; only when the person says so'),
158
+ }),
159
+ annotations: OPERATE,
160
+ },
161
+ (params) =>
162
+ runTool(async () => {
163
+ const moved = await client.moveProduct(params.productId, {
164
+ factoryId: params.factoryId,
165
+ ...(params.stopLine ? { stopLine: true } : {}),
166
+ });
167
+ return toolResult({
168
+ product: productRow(moved.data as Row),
169
+ changed: moved.changed,
170
+ previousFactoryId: moved.previousFactoryId,
171
+ lineStopped: moved.lineStopped,
172
+ failedDocuments: moved.failedDocuments,
173
+ });
174
+ })
175
+ );
176
+ }
177
+
178
+ if (!client.hasCapability('products:write')) return;
179
+
180
+ server.registerTool(
181
+ 'taiga_create_product',
182
+ {
183
+ title: 'Create a product',
184
+ description:
185
+ 'Create a product in a factory, the way New product does. It starts one of two ways, chosen now and not ' +
186
+ 'later: scratch, where Discovery is a conversation that writes the specification (held in the web), or ' +
187
+ 'import (projectType brownfield), where Taiga reads an existing repository and drafts the documents from ' +
188
+ 'the code: link the repository (taiga_link_repository; linking is for owners and admins, so a member ' +
189
+ 'asks one), then taiga_start_import. Also what Taiga builds for it: its ' +
190
+ 'infrastructure code and its CI/CD pipelines, each following the organization unless decided here; when ' +
191
+ 'one is off, deploymentNotes says in a sentence how that half is done today. Material the product should ' +
192
+ 'be built from is worth adding before Discovery starts (taiga_add_knowledge with the productId): what is ' +
193
+ 'added first shapes the specification.',
194
+ inputSchema: z.object({
195
+ factoryId,
196
+ name: z.string().trim().min(1).max(255).describe('The product name'),
197
+ start: z.enum(['scratch', 'import']).describe('scratch: describe it in Discovery; import: from a repository'),
198
+ ...buildScope,
199
+ deploymentNotes: deploymentNotes.optional(),
200
+ }),
201
+ annotations: WRITE,
202
+ },
203
+ (params) =>
204
+ runTool(async () => {
205
+ const contradiction = notesWithNothingToDescribe(params);
206
+ if (contradiction) return contradiction;
207
+ const product = (
208
+ await client.createProduct({
209
+ name: params.name,
210
+ factoryId: params.factoryId,
211
+ ...(params.start === 'import' ? { projectType: 'brownfield' as const } : {}),
212
+ ...(params.ownsInfrastructure !== undefined ? { ownsInfrastructure: params.ownsInfrastructure } : {}),
213
+ ...(params.ownsDeployment !== undefined ? { ownsDeployment: params.ownsDeployment } : {}),
214
+ ...(params.deploymentNotes !== undefined ? { deploymentNotes: params.deploymentNotes } : {}),
215
+ })
216
+ ).data as Row;
217
+ return toolResult(productRow(product));
218
+ })
219
+ );
220
+
221
+ server.registerTool(
222
+ 'taiga_update_product',
223
+ {
224
+ title: "Change a product's settings",
225
+ description:
226
+ "Change a product's name (its slug follows the name, so links to the product change) or what Taiga " +
227
+ 'builds for it: its infrastructure code and CI/CD pipelines (null ' +
228
+ 'returns one to the organization default), the sentence on how a half Taiga does not build is done today, ' +
229
+ "and whether it inherits the organization's design standards (null follows the default for how the " +
230
+ 'product started). Later plans read the new answer; work already planned is not redone. Autonomy is ' +
231
+ 'taiga_set_product_autonomy. Ask the person first.',
232
+ inputSchema: z.object({
233
+ productId,
234
+ name: z.string().trim().min(1).max(255).optional().describe('A new name'),
235
+ ...buildScope,
236
+ deploymentNotes: deploymentNotes.nullable().optional(),
237
+ emitDesignStandards: z
238
+ .boolean()
239
+ .nullable()
240
+ .optional()
241
+ .describe("Inherit the organization's design standards; null follows the default"),
242
+ }),
243
+ annotations: OPERATE_SETTING,
244
+ },
245
+ (params) =>
246
+ runTool(async () => {
247
+ const { productId: id, ...changes } = params;
248
+ if (Object.values(changes).every((value) => value === undefined)) {
249
+ return toolError('VALIDATION_FAILED', 'Change at least one setting.');
250
+ }
251
+ const contradiction = notesWithNothingToDescribe(params);
252
+ if (contradiction) return contradiction;
253
+ return toolResult(productRow((await client.updateProduct(id, changes)).data as Row));
254
+ })
255
+ );
256
+
257
+ server.registerTool(
258
+ 'taiga_refresh_reality_documents',
259
+ {
260
+ title: "Refresh a product's documents from its code",
261
+ description:
262
+ "Derive the reality documents again from the code on the branch Taiga works on (the link's effectiveBranch: " +
263
+ "the chosen branch, or the repository's default): all of them, or the ones named (with " +
264
+ 'what they are derived from). For a document the import misread, refresh it rather than importing ' +
265
+ 'again; unchanged sections are not republished. Runs an agent in the background and returns the job; ' +
266
+ 'taiga_wait_for_job says when it is done. Ask the person first.',
267
+ inputSchema: z.object({
268
+ productId,
269
+ docTypes: z
270
+ .array(z.enum(REALITY_DERIVED_DOCUMENT_TYPES))
271
+ .min(1)
272
+ .max(REALITY_DERIVED_DOCUMENT_TYPES.length)
273
+ .optional()
274
+ .describe('The documents to refresh; omit for all'),
275
+ }),
276
+ annotations: OPERATE,
277
+ },
278
+ (params) =>
279
+ runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data))
280
+ );
281
+
282
+ server.registerTool(
283
+ 'taiga_finish_discovery',
284
+ {
285
+ title: 'Finish Discovery',
286
+ description:
287
+ "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the " +
288
+ 'required documents still unpublished (details.missingDocuments); not_in_setup means it is already ' +
289
+ 'finished; a required check left unstamped (stamped in the web) is refused with its details too. It ' +
290
+ 'locks the document set ' +
291
+ 'and opens the rest of the product; for an ' +
292
+ 'imported product it is also when the first initiatives are planned. A locked document changes only ' +
293
+ 'after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.',
294
+ inputSchema: z.object({ productId }),
295
+ annotations: OPERATE,
296
+ },
297
+ (params) => runTool(async () => toolResult(productRow((await client.completeSetup(params.productId)).data as Row)))
298
+ );
299
+
300
+ server.registerTool(
301
+ 'taiga_reopen_discovery',
302
+ {
303
+ title: 'Reopen Discovery',
304
+ description:
305
+ "Reopen a finished product's Discovery so its documents can be amended; finish it again afterwards. " +
306
+ 'Nothing built from them is undone. Ask the person first.',
307
+ inputSchema: z.object({ productId }),
308
+ annotations: OPERATE,
309
+ },
310
+ (params) => runTool(async () => toolResult(productRow((await client.reopenSetup(params.productId)).data as Row)))
311
+ );
312
+
313
+ if (!client.hasCapability('products:read')) return;
314
+
315
+ server.registerTool(
316
+ 'taiga_start_import',
317
+ {
318
+ title: 'Import a codebase into a product',
319
+ description:
320
+ 'Start the import of a product created to import a codebase: Taiga reads its linked repository and ' +
321
+ 'derives the full document set from the code, each document citing the files it came from. Importing ' +
322
+ 'is one decision; the rest runs by itself. The scanners then sweep the repository, the documents are ' +
323
+ "assessed against the organization's policies, and the first initiatives are planned when Discovery is " +
324
+ 'finished. Refused for a product started from scratch (the choice is made at creation), without a ' +
325
+ 'linked repository, or while an import runs. Runs agents in the background and returns the job; ' +
326
+ 'taiga_wait_for_job says when it is done (it takes a while). Ask the person first.',
327
+ inputSchema: z.object({ productId }),
328
+ annotations: OPERATE,
329
+ },
330
+ (params) =>
331
+ runTool(async () => {
332
+ const product = (await client.getProduct(params.productId)).data as Row;
333
+ if (!isImportedProduct(product as ProductSettingsView)) {
334
+ return toolError(
335
+ 'CONFLICT',
336
+ 'This product was started from scratch, so it has no import: the choice is made when a product is created.'
337
+ );
338
+ }
339
+ return toolResult((await client.startImport(params.productId)).data);
340
+ })
341
+ );
342
+ }