@taiga-ai/mcp-server 0.25.0-rc.472 → 0.26.0-dev.479

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 (55) hide show
  1. package/README.md +111 -0
  2. package/dist/index.js +322 -122
  3. package/package.json +15 -2
  4. package/scripts/build.mjs +0 -37
  5. package/scripts/prepare-publish.cjs +0 -49
  6. package/src/client.test.ts +0 -424
  7. package/src/client.ts +0 -976
  8. package/src/config.test.ts +0 -24
  9. package/src/config.ts +0 -28
  10. package/src/index.ts +0 -45
  11. package/src/modern.integration.test.ts +0 -153
  12. package/src/server.integration.test.ts +0 -9075
  13. package/src/server.ts +0 -18
  14. package/src/tools/annotations.ts +0 -38
  15. package/src/tools/autonomy.ts +0 -86
  16. package/src/tools/backlogs.ts +0 -766
  17. package/src/tools/builds.ts +0 -668
  18. package/src/tools/catalogue.test.ts +0 -170
  19. package/src/tools/catalogue.ts +0 -223
  20. package/src/tools/context.ts +0 -513
  21. package/src/tools/documents.ts +0 -375
  22. package/src/tools/errors.ts +0 -172
  23. package/src/tools/hints.ts +0 -99
  24. package/src/tools/inbox.ts +0 -546
  25. package/src/tools/index.ts +0 -239
  26. package/src/tools/inputs.ts +0 -47
  27. package/src/tools/instructions.ts +0 -41
  28. package/src/tools/jobs.ts +0 -90
  29. package/src/tools/knowledge.ts +0 -249
  30. package/src/tools/maintenance.ts +0 -540
  31. package/src/tools/markdown.test.ts +0 -249
  32. package/src/tools/markdown.ts +0 -151
  33. package/src/tools/observe.ts +0 -311
  34. package/src/tools/product-documents.ts +0 -184
  35. package/src/tools/products.ts +0 -529
  36. package/src/tools/prompts.ts +0 -265
  37. package/src/tools/queue.ts +0 -250
  38. package/src/tools/readiness.ts +0 -177
  39. package/src/tools/remote.test.ts +0 -44
  40. package/src/tools/remote.ts +0 -107
  41. package/src/tools/repositories.ts +0 -360
  42. package/src/tools/request-prompt.test.ts +0 -32
  43. package/src/tools/request-prompt.ts +0 -28
  44. package/src/tools/resources.ts +0 -217
  45. package/src/tools/sections.test.ts +0 -143
  46. package/src/tools/sections.ts +0 -287
  47. package/src/tools/setup.ts +0 -455
  48. package/src/tools/summarize.test.ts +0 -20
  49. package/src/tools/summarize.ts +0 -96
  50. package/src/tools/wait.ts +0 -490
  51. package/src/version.test.ts +0 -10
  52. package/src/version.ts +0 -14
  53. package/taiga-ai-mcp-server-0.25.0-rc.472.tgz +0 -0
  54. package/tsconfig.json +0 -21
  55. package/vitest.config.ts +0 -9
@@ -1,170 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server';
3
- import { Client } from '@modelcontextprotocol/client';
4
- import {
5
- FACTORY_DOCUMENT_TYPES,
6
- POLICY_TYPES,
7
- PRODUCT_DOCUMENT_TYPES,
8
- PRODUCT_STATUSES,
9
- TENANT_DOCUMENT_TYPES,
10
- } from '@taiga/types';
11
- import { TaigaClient } from '../client.js';
12
- import { registerAllTools, writeNotice } from './index.js';
13
- import { SERVER_INSTRUCTIONS, SERVER_INSTRUCTIONS_LIMIT } from './instructions.js';
14
- import { PRODUCT_DOCUMENT_NAMES } from './catalogue.js';
15
-
16
- /**
17
- * What the model reads must match what exists. The walk-through that led to
18
- * this file found two policy types named in a description that no tenant
19
- * has; a model that follows a description into a type the API refuses learns
20
- * to stop asking.
21
- */
22
-
23
- const ALL_CAPABILITIES = [
24
- 'products:read',
25
- 'factories:read',
26
- 'factories:documents:write',
27
- 'tenant:documents:read',
28
- 'tenant:documents:write',
29
- 'backlog:read',
30
- 'backlog:write',
31
- 'products:write',
32
- 'customDocuments:read',
33
- 'customDocuments:write',
34
- 'customDocuments:delete',
35
- 'factories:write',
36
- 'vcs:read',
37
- 'vcs:manage',
38
- ];
39
-
40
- async function connected() {
41
- vi.stubGlobal(
42
- 'fetch',
43
- vi.fn().mockResolvedValue({
44
- ok: true,
45
- status: 200,
46
- json: async () => ({
47
- data: { userId: 'u', tenantId: 't', email: 'e', role: 'owner', capabilities: ALL_CAPABILITIES },
48
- }),
49
- text: async () => '',
50
- })
51
- );
52
- const taigaClient = new TaigaClient({ baseUrl: 'https://api.test.com', apiKey: 'k' });
53
- await taigaClient.introspect();
54
- const server = new McpServer({ name: 't', version: '0' });
55
- registerAllTools(server, taigaClient);
56
- const client = new Client({ name: 'c', version: '0' });
57
- const [ct, st] = InMemoryTransport.createLinkedPair();
58
- await Promise.all([server.connect(st), client.connect(ct)]);
59
- return client;
60
- }
61
-
62
- async function allTools() {
63
- return (await (await connected()).listTools()).tools;
64
- }
65
-
66
- /** Every description a client sees: tools, resource templates, prompts and their arguments. */
67
- async function everyDescription(): Promise<string> {
68
- const client = await connected();
69
- const [tools, templates, prompts] = await Promise.all([
70
- client.listTools(),
71
- client.listResourceTemplates(),
72
- client.listPrompts(),
73
- ]);
74
- return JSON.stringify([tools.tools, templates.resourceTemplates, prompts.prompts]);
75
- }
76
-
77
- /** Every `policy_*` token in a text. */
78
- const policyTokens = (text: string) => [...new Set(text.match(/\bpolicy_[a-z_]+\b/g) ?? [])];
79
-
80
- describe('tool descriptions against the catalogue', () => {
81
- beforeEach(() => vi.restoreAllMocks());
82
-
83
- it('names no policy type the catalogue lacks, in any description, schema, template or prompt', async () => {
84
- const text = (await everyDescription()) + SERVER_INSTRUCTIONS;
85
-
86
- for (const token of policyTokens(text)) {
87
- expect(TENANT_DOCUMENT_TYPES as readonly string[], token).toContain(token);
88
- }
89
- });
90
-
91
- it('names no tool that is not registered, in any description, template, prompt or the instructions', async () => {
92
- const client = await connected();
93
- const registered = new Set((await client.listTools()).tools.map((t) => t.name));
94
- const text = (await everyDescription()) + SERVER_INSTRUCTIONS;
95
-
96
- for (const name of new Set(text.match(/\btaiga_[a-z_]+\b/g) ?? [])) {
97
- // Prompts carry the same prefix; they are registered too.
98
- const prompts = new Set((await client.listPrompts()).prompts.map((p) => p.name));
99
- expect(registered.has(name) || prompts.has(name), name).toBe(true);
100
- }
101
- });
102
-
103
- it('names every policy where the company documents are explained', async () => {
104
- const tools = await allTools();
105
- // One tool lists and reads the company documents.
106
- const get = tools.find((t) => t.name === 'taiga_get_document')!;
107
- const list = get;
108
- const typeEnum = (get.inputSchema.properties as Record<string, { enum?: string[] }>)['type']!.enum;
109
-
110
- for (const type of POLICY_TYPES) {
111
- expect(list.description, type).toContain(type);
112
- }
113
- expect(typeEnum).toEqual([...TENANT_DOCUMENT_TYPES]);
114
- });
115
-
116
- it('offers exactly the product statuses, product document types and origins the API has', async () => {
117
- const tools = await allTools();
118
- const props = (name: string) =>
119
- tools.find((t) => t.name === name)!.inputSchema.properties as Record<string, { enum?: string[] }>;
120
-
121
- expect(props('taiga_list_products')['status']!.enum).toEqual([...PRODUCT_STATUSES]);
122
- expect(props('taiga_get_product_document')['type']!.enum).toEqual([...PRODUCT_DOCUMENT_TYPES]);
123
- expect(props('taiga_get_product_document')['origin']!.enum).toEqual(['plan', 'reality']);
124
- expect(props('taiga_get_factory_document')['type']!.enum).toEqual([...FACTORY_DOCUMENT_TYPES]);
125
- });
126
-
127
- it('has a display name for every product document type, the one the web uses', () => {
128
- expect(Object.keys(PRODUCT_DOCUMENT_NAMES).sort()).toEqual([...PRODUCT_DOCUMENT_TYPES].sort());
129
- // apps/web/src/lib/documentTypes.ts: "Look & Feel", not "UI Direction".
130
- expect(PRODUCT_DOCUMENT_NAMES.ui_direction).toBe('Look & Feel');
131
- });
132
-
133
- it('explains both timelines where a product document is read', async () => {
134
- const tools = await allTools();
135
- const origin = (
136
- tools.find((t) => t.name === 'taiga_get_product_document')!.inputSchema.properties as Record<
137
- string,
138
- { description?: string }
139
- >
140
- )['origin']!.description;
141
- expect(origin).toMatch(/plan: .*discovery/);
142
- expect(origin).toMatch(/reality: .*merged pull request/);
143
- });
144
-
145
- it('fits the whole instructions inside what Claude Code keeps, and says where to start and when to ask', () => {
146
- // Claude Code loads only tool names and the instructions until the model
147
- // searches, and cuts the instructions at 2,048 characters (PRO-2800).
148
- expect(SERVER_INSTRUCTIONS.length).toBeLessThanOrEqual(SERVER_INSTRUCTIONS_LIMIT);
149
- for (const must of ['taiga_get_product_context', 'ask-first', 'NOT_FOUND', 'initiative', 'Only in the web']) {
150
- expect(SERVER_INSTRUCTIONS).toContain(must);
151
- }
152
- });
153
- });
154
-
155
- // PRO-2778: the notice is what tells the model a tool writes, so a tool must say whether it does.
156
- describe('writeNotice', () => {
157
- it('refuses a tool whose annotations do not say whether it is read-only', () => {
158
- expect(() => writeNotice('taiga_x', undefined)).toThrow(/taiga_x: annotations must say/);
159
- expect(() => writeNotice('taiga_x', {})).toThrow(/annotations must say/);
160
- });
161
-
162
- it('says nothing for a read, and what a write does otherwise', () => {
163
- expect(writeNotice('r', { readOnlyHint: true })).toBe('');
164
- expect(writeNotice('w', { readOnlyHint: false })).toBe('Writes. ');
165
- expect(writeNotice('o', { readOnlyHint: false, destructiveHint: true })).toBe('Ask first (others see it). ');
166
- expect(writeNotice('taiga_queue_initiative', { readOnlyHint: false, destructiveHint: true })).toBe(
167
- 'Ask first (paid agent work). '
168
- );
169
- });
170
- });
@@ -1,223 +0,0 @@
1
- /**
2
- * What exists, from the one place that knows: the shared types package.
3
- *
4
- * Tool descriptions are what the model reads to decide what to call, and a
5
- * description that names a document type the catalogue lacks sends it down a
6
- * dead end (the walk-through that led here found `policy_acceptable_use` and
7
- * `policy_ip` named in a description and nowhere else). Every list of types,
8
- * origins and statuses a tool shows is derived from these constants, and a
9
- * test holds the descriptions to them.
10
- */
11
- import {
12
- FACTORY_DOCUMENT_TYPES,
13
- POLICY_TYPES,
14
- PRODUCT_DOCUMENT_DISPLAY_NAMES,
15
- PRODUCT_DOCUMENT_ORIGINS,
16
- PRODUCT_DOCUMENT_TYPES,
17
- PRODUCT_STATUSES,
18
- REALITY_DERIVED_DOCUMENT_TYPES,
19
- TENANT_DOCUMENT_TYPES,
20
- type ProductDocumentOrigin,
21
- type ProductDocumentType,
22
- type TenantDocumentType,
23
- } from '@taiga/types';
24
-
25
- export {
26
- FACTORY_DOCUMENT_TYPES,
27
- POLICY_TYPES,
28
- PRODUCT_DOCUMENT_ORIGINS,
29
- PRODUCT_DOCUMENT_TYPES,
30
- PRODUCT_STATUSES,
31
- TENANT_DOCUMENT_TYPES,
32
- };
33
- export type { ProductDocumentOrigin, ProductDocumentType };
34
-
35
- /**
36
- * Display names for every product document type. The reviewable nine come
37
- * from the shared map the web uses; the three the web never asks a person to
38
- * review are named here, and the record type keeps the two sets complete.
39
- */
40
- export const PRODUCT_DOCUMENT_NAMES: Record<ProductDocumentType, string> = {
41
- ...PRODUCT_DOCUMENT_DISPLAY_NAMES,
42
- // "Look & Feel" is what the web calls it (apps/web/src/lib/documentTypes.ts);
43
- // an agent and a person should name the same document the same way.
44
- ui_direction: 'Look & Feel',
45
- codebase_snapshot: 'Codebase Snapshot',
46
- product_instructions: 'Product Instructions',
47
- };
48
-
49
- export function productDocumentName(type: string): string {
50
- return (PRODUCT_DOCUMENT_NAMES as Record<string, string>)[type] ?? type;
51
- }
52
-
53
- /** The organization documents that are not policies: goals, standards, voice, instructions. */
54
- export const OTHER_TENANT_DOCUMENT_TYPES: readonly TenantDocumentType[] = TENANT_DOCUMENT_TYPES.filter(
55
- (type) => !(POLICY_TYPES as readonly string[]).includes(type)
56
- );
57
-
58
- /** `a, b, c` for a description. */
59
- export function named(types: readonly string[]): string {
60
- return types.join(', ');
61
- }
62
-
63
- /**
64
- * The two document timelines of a product (ADR-017), in the words the tools
65
- * and the server instructions use. One place, so the list tool, the read
66
- * tool and the instructions cannot explain them three different ways.
67
- */
68
- export const ORIGIN_MEANING: Record<ProductDocumentOrigin, string> = {
69
- plan: 'plan: the discovery documents, what is meant to be built.',
70
- reality:
71
- 'reality: derived from the code on the main branch, what is built; an imported product has them from ' +
72
- 'its import, any other once its first initiative is done (a merged pull request) or code is pushed.',
73
- };
74
-
75
- /**
76
- * Which timeline is a product's own (the API's documentOriginForSettings):
77
- * an imported product has no plan chain by design, so its documents are on
78
- * the reality side; everything else starts on the plan side. Derived from the
79
- * same settings key the API reads, so the two cannot disagree.
80
- */
81
- export function isImportedProduct(product: ProductSettingsView | null | undefined): boolean {
82
- const settings = product?.settings;
83
- return (
84
- typeof settings === 'object' &&
85
- settings !== null &&
86
- (settings as { projectType?: unknown }).projectType === 'brownfield'
87
- );
88
- }
89
-
90
- /**
91
- * The document types no derivation ever writes: a person records or writes
92
- * them, so they live on the plan side of every product, imported or not.
93
- */
94
- export const PLAN_ONLY_DOCUMENT_TYPES: readonly ProductDocumentType[] = PRODUCT_DOCUMENT_TYPES.filter(
95
- (type) => !(REALITY_DERIVED_DOCUMENT_TYPES as readonly string[]).includes(type)
96
- );
97
-
98
- /**
99
- * The timeline a read should default to: the plan side for a type only a
100
- * person writes, else the product's own side (reality for an imported
101
- * product, plan for every other).
102
- */
103
- export function defaultOriginFor(
104
- product: ProductSettingsView | null | undefined,
105
- type?: ProductDocumentType
106
- ): ProductDocumentOrigin {
107
- if (type !== undefined && PLAN_ONLY_DOCUMENT_TYPES.includes(type)) return 'plan';
108
- return isImportedProduct(product) ? 'reality' : 'plan';
109
- }
110
-
111
- /** The product settings the notes read: the import marker and where the import got to. */
112
- export interface ProductSettingsView {
113
- settings?: unknown;
114
- }
115
-
116
- /**
117
- * Where an imported product's import got to (`settings.importStatus`, written
118
- * by the API: pending, analyzing, complete, cancelled, failed). Unknown values
119
- * read as "not known", never as a promise.
120
- */
121
- function importStatusOf(product: ProductSettingsView | null | undefined): string | null {
122
- const settings = product?.settings;
123
- if (typeof settings !== 'object' || settings === null) return null;
124
- const status = (settings as { importStatus?: unknown }).importStatus;
125
- return typeof status === 'string' ? status : null;
126
- }
127
-
128
- /** Why a side of the timeline is empty, for a list or a miss that shows one. */
129
- export function emptyOriginNote(
130
- origin: ProductDocumentOrigin,
131
- product: ProductSettingsView | null | undefined
132
- ): string {
133
- const imported = isImportedProduct(product);
134
- if (origin === 'plan') {
135
- return imported
136
- ? 'This product was imported, so no discovery chain was generated for it: its derived documents are on ' +
137
- `the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(', ')}) ` +
138
- 'would appear here.'
139
- : 'No plan documents are published yet: they are written during discovery in Taiga.';
140
- }
141
- if (!imported) {
142
- return (
143
- 'No reality documents yet: they are derived from the code once the first initiative is done (a merged ' +
144
- 'pull request marks it done) or the repository is pushed to. Until then read the plan side.'
145
- );
146
- }
147
- switch (importStatusOf(product)) {
148
- case 'pending':
149
- return 'No reality documents yet: the import has not started. Link the repository, then taiga_start_import.';
150
- // generating_docs is the retired name for the same in-flight state
151
- // (PRO-1161); rows that predate it can still carry it.
152
- case 'analyzing':
153
- case 'generating_docs':
154
- return 'No reality documents yet: the import is running and will derive them from the code.';
155
- case 'cancelled':
156
- return 'No reality documents: the import was cancelled before it derived any. taiga_start_import runs it again.';
157
- case 'failed':
158
- return 'No reality documents: the import failed before it derived any. taiga_start_import runs it again.';
159
- default:
160
- return 'No reality documents: the import derives them from the code, and none have been published.';
161
- }
162
- }
163
-
164
- /**
165
- * What a product in setup waits for, as the next call to make. Nothing on a
166
- * draft product says it otherwise: discoveryStep names the step and
167
- * importStatus the import, and the way on from each lives in tool
168
- * descriptions an agent reads only when it already knows which. Null once the
169
- * product is active.
170
- */
171
- export function setupNextStep(
172
- product: (ProductSettingsView & { status?: unknown; discoveryStep?: unknown }) | null | undefined
173
- ): string | null {
174
- if (product?.status !== 'draft') return null;
175
- if (isImportedProduct(product)) {
176
- switch (importStatusOf(product)) {
177
- case 'analyzing':
178
- case 'generating_docs':
179
- return (
180
- 'The import is running and derives the documents from the code (taiga_wait_for_job follows its job). ' +
181
- 'Then review them, and taiga_finish_discovery opens the product.'
182
- );
183
- case 'complete':
184
- return (
185
- 'The import is done: review the reality documents (taiga_get_product_document without type), then ' +
186
- 'taiga_finish_discovery opens the product.'
187
- );
188
- case 'failed':
189
- case 'cancelled':
190
- return 'The last import did not finish: taiga_start_import runs it again.';
191
- default:
192
- return (
193
- 'Link its repository if it has none (taiga_list_linkable_repositories, taiga_link_repository), then ' +
194
- 'taiga_start_import: the import derives the documents from the code and runs by itself.'
195
- );
196
- }
197
- }
198
- // Unknown is not "not started": a write's answer leaves the step out, and
199
- // the default below would send a product at its documents back to the
200
- // conversation.
201
- if (product.discoveryStep === undefined) return null;
202
- switch (product.discoveryStep) {
203
- case 'documents':
204
- return (
205
- 'The specification is done; the other documents are generated from it ' +
206
- '(taiga_generate_product_documents starts the ones still missing). Once every required one is ' +
207
- 'published, taiga_finish_discovery opens the product.'
208
- );
209
- case 'checks':
210
- return 'The documents are published; the checks are stamped in the web, then taiga_finish_discovery opens the product.';
211
- default:
212
- return (
213
- 'Discovery is a conversation held in the web: the person describes the product there and the ' +
214
- 'specification comes out of it. The other documents follow, then taiga_finish_discovery opens the product.'
215
- );
216
- }
217
- }
218
-
219
- /** What discoveryStep says, for a description that shows it. */
220
- export const DISCOVERY_STEP_MEANING =
221
- 'discoveryStep is where a product in setup is: context (not started), conversation or import (the ' +
222
- 'specification being written or derived from code), documents (generated and reviewed), checks (stamped in ' +
223
- 'the web); null once it is active.';