@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,143 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- applySectionEdit,
4
- budgetSections,
5
- instructionsText,
6
- pickSections,
7
- sectionContent,
8
- sectionsOf,
9
- stripProvenance,
10
- } from './sections.js';
11
-
12
- describe('sectionsOf', () => {
13
- it('returns the sections map of a section-shaped document', () => {
14
- expect(sectionsOf({ sections: { a: { content: 'x' } } })).toEqual({ a: { content: 'x' } });
15
- });
16
-
17
- it('returns null for documents that keep their content elsewhere', () => {
18
- expect(sectionsOf({ components: {} })).toBeNull();
19
- expect(sectionsOf({ sections: 'text' })).toBeNull();
20
- expect(sectionsOf({ sections: ['a'] })).toBeNull();
21
- });
22
- });
23
-
24
- describe('sectionContent', () => {
25
- it('reads object and bare-string sections alike', () => {
26
- expect(sectionContent({ a: { content: 'x' } }, 'a')).toBe('x');
27
- expect(sectionContent({ a: 'plain' }, 'a')).toBe('plain');
28
- });
29
-
30
- it('is empty for a section without text and null for a missing or null one', () => {
31
- expect(sectionContent({ a: { priority: 'core' } }, 'a')).toBe('');
32
- expect(sectionContent({}, 'a')).toBeNull();
33
- expect(sectionContent({ a: null }, 'a')).toBeNull();
34
- });
35
- });
36
-
37
- describe('applySectionEdit', () => {
38
- const sections = {
39
- summary: { source: 'template', priority: 'core', content: 'old', derivedFrom: ['doc-1'] },
40
- scope: 'bare string',
41
- list: ['not', 'a', 'section'],
42
- };
43
-
44
- it('replaces the text and keeps the section metadata and the other sections', () => {
45
- expect(applySectionEdit(sections, 'summary', 'new')).toEqual({
46
- summary: { source: 'template', priority: 'core', content: 'new', derivedFrom: ['doc-1'] },
47
- scope: 'bare string',
48
- list: ['not', 'a', 'section'],
49
- });
50
- });
51
-
52
- it('turns a bare-string or array-valued section into an object with the new text', () => {
53
- expect(applySectionEdit(sections, 'scope', 'new')['scope']).toEqual({ content: 'new' });
54
- expect(applySectionEdit(sections, 'list', 'new')['list']).toEqual({ content: 'new' });
55
- });
56
-
57
- it('adds a section that did not exist', () => {
58
- expect(applySectionEdit(sections, 'retention', 'keep 90 days')['retention']).toEqual({ content: 'keep 90 days' });
59
- });
60
-
61
- it('does not mutate its input', () => {
62
- applySectionEdit(sections, 'summary', 'new');
63
-
64
- expect(sections.summary.content).toBe('old');
65
- });
66
- });
67
-
68
- describe('instructionsText', () => {
69
- it('joins every non-empty object section and skips bare strings, as the API extractor does', () => {
70
- expect(
71
- instructionsText({
72
- instructions: { content: ' Use pnpm. ' },
73
- legacy: 'bare text the agents never receive',
74
- empty: { content: ' ' },
75
- deploys: { content: 'Deploy on Tuesdays.' },
76
- })
77
- ).toBe('Use pnpm.\n\nDeploy on Tuesdays.');
78
- });
79
-
80
- it('is null when nothing is readable', () => {
81
- expect(instructionsText({ empty: { content: '' }, legacy: 'x' })).toBeNull();
82
- });
83
- });
84
-
85
- describe('stripProvenance', () => {
86
- it('drops the derived-from tree and outdated sources at the top and in every section', () => {
87
- const doc = {
88
- id: 'd1',
89
- type: 'policy_security',
90
- derivedFrom: { sources: [{ type: 'business_goals', version: 2 }] },
91
- outdated: { sources: [] },
92
- data: {
93
- version: '1.0.0',
94
- sections: {
95
- summary: { source: 'template', priority: 'core', content: 'x', derivedFrom: ['business_goals'] },
96
- legacy: 'bare text',
97
- },
98
- },
99
- };
100
-
101
- expect(stripProvenance(doc)).toEqual({
102
- id: 'd1',
103
- type: 'policy_security',
104
- data: {
105
- version: '1.0.0',
106
- sections: { summary: { source: 'template', priority: 'core', content: 'x' }, legacy: 'bare text' },
107
- },
108
- });
109
- });
110
-
111
- it('leaves a document that is not made of sections alone', () => {
112
- const doc = { id: 'd2', type: 'ui_direction', data: { picks: { palette: 'ocean' } } };
113
-
114
- expect(stripProvenance(doc)).toEqual(doc);
115
- });
116
- });
117
-
118
- describe('budgetSections and pickSections', () => {
119
- // Parsed, as the API's JSON arrives: an own "__proto__" key, not a prototype.
120
- const doc = JSON.parse(
121
- '{"data":{"sections":{"__proto__":{"content":"aaaa"},"constructor":{"content":"bbbb"},"toString":{"content":"cccc"}}}}'
122
- ) as Record<string, unknown>;
123
-
124
- it('keeps a section named like an Object.prototype member as an ordinary section', () => {
125
- const cut = budgetSections(doc, 8)!;
126
- const kept = (cut.doc.data as { sections: Record<string, unknown> }).sections;
127
-
128
- expect(Object.keys(kept)).toEqual(['__proto__', 'constructor']);
129
- expect(Object.getPrototypeOf(kept)).toBe(Object.prototype);
130
- expect(cut.omittedSections).toEqual([{ id: 'toString', characters: 4 }]);
131
- });
132
-
133
- it('lists every section not picked, including one named like a prototype member', () => {
134
- const cut = pickSections(doc, new Set(['toString']))!;
135
-
136
- expect(cut.omittedSections.map((s) => s.id)).toEqual(['__proto__', 'constructor']);
137
- });
138
-
139
- it('answers null for a document that is not made of sections', () => {
140
- expect(budgetSections({ data: { legacy: 'x' } }, 10)).toBeNull();
141
- expect(pickSections({ data: { legacy: 'x' } }, new Set(['a']))).toBeNull();
142
- });
143
- });
@@ -1,287 +0,0 @@
1
- /**
2
- * How a section edit changes a document's `data`.
3
- *
4
- * Policies, organization instructions and factory instructions keep their
5
- * content under `data.sections`, one entry per section id. An edit replaces
6
- * that section's text and nothing else: `priority` (ordering), `source` and
7
- * `derivedFrom` (which reference documents shaped it, PRO-2158) stay, so the
8
- * next derivation does not re-add what an earlier one already brought in.
9
- * Same rule as the web editor (apps/web/src/features/organization/lib/
10
- * policySectionEdits.ts); the two must agree or a draft saved from one
11
- * surface loses what the other preserved.
12
- */
13
-
14
- import type { CallToolResult } from '@modelcontextprotocol/server';
15
- import { z } from 'zod';
16
- import { CONTROL_ID_SOURCE, isControlId, policyNamedByControl, policyTypeForControl } from '@taiga/types';
17
- import { toolError, toolResult } from './errors.js';
18
-
19
- /** A document's section map, keyed by section id: each value a `StoredSection` or (older rows) a bare string. */
20
- export type Sections = Record<string, unknown>;
21
-
22
- /** A section stored as an object: its text and the metadata an edit keeps. */
23
- export interface StoredSection {
24
- content?: unknown;
25
- title?: unknown;
26
- priority?: unknown;
27
- [key: string]: unknown;
28
- }
29
-
30
- /** A document's `data`: a section-shaped document keeps its content under `sections`. */
31
- export interface DocumentData {
32
- sections?: unknown;
33
- [key: string]: unknown;
34
- }
35
-
36
- /** A document as these helpers read it: its `data` and the provenance a reader does not need. */
37
- export interface StoredDocument {
38
- data?: unknown;
39
- derivedFrom?: unknown;
40
- outdated?: unknown;
41
- [key: string]: unknown;
42
- }
43
-
44
- export function isRecord(value: unknown): value is Record<string, unknown> {
45
- return typeof value === 'object' && value !== null && !Array.isArray(value);
46
- }
47
-
48
- /** A section stored as an object rather than a bare string. */
49
- export function isSectionObject(value: unknown): value is StoredSection {
50
- return isRecord(value);
51
- }
52
-
53
- /** The `sections` map of a document, or null when the document is not section-shaped. */
54
- export function sectionsOf(data: DocumentData): Sections | null {
55
- return isRecord(data.sections) ? data.sections : null;
56
- }
57
-
58
- /** The text of one section, or null when the section does not exist. */
59
- export function sectionContent(sections: Sections, sectionId: string): string | null {
60
- const section = sections[sectionId];
61
- if (section === undefined || section === null) return null;
62
- if (typeof section === 'string') return section;
63
- return isSectionObject(section) && typeof section.content === 'string' ? section.content : '';
64
- }
65
-
66
- /** The map with one section's text replaced (or the section added); every other section untouched. */
67
- export function applySectionEdit(sections: Sections, sectionId: string, content: string): Sections {
68
- const prior = sections[sectionId];
69
- return { ...sections, [sectionId]: isRecord(prior) ? { ...prior, content } : { content } };
70
- }
71
-
72
- /**
73
- * A document for reading, without its provenance: the top-level `derivedFrom`
74
- * tree and `outdated` source list (PRO-2359), and each section's own
75
- * `derivedFrom` (PRO-2158). They say which documents shaped this one, which
76
- * the editor needs and a reader does not; `source` and `priority` stay, the
77
- * server instructions explain them.
78
- */
79
- export function stripProvenance<T extends StoredDocument>(doc: T): Omit<T, 'derivedFrom' | 'outdated'> {
80
- const { derivedFrom: _derivedFrom, outdated: _outdated, ...rest } = doc;
81
- // `rest` holds the same `data`; read through `doc`, whose type still names it.
82
- const { data } = doc;
83
- if (!isRecord(data)) return rest;
84
- const sections = sectionsOf(data);
85
- if (!sections) return rest;
86
- const stripped = Object.fromEntries(
87
- Object.entries(sections).map(([id, section]) => {
88
- if (!isRecord(section)) return [id, section];
89
- const { derivedFrom: _sectionDerivedFrom, ...keep } = section;
90
- return [id, keep];
91
- })
92
- );
93
- return { ...rest, data: { ...data, sections: stripped } };
94
- }
95
-
96
- /**
97
- * The free-form text of an instructions document, the way the API hands it
98
- * to the agents (factory-instructions.ts: every non-empty section, in order,
99
- * joined by a blank line). Null when there is nothing to read.
100
- */
101
- export function instructionsText(sections: Sections): string | null {
102
- const parts: string[] = [];
103
- for (const section of Object.values(sections)) {
104
- // Object sections only, as the API's extractor reads them: a bare-string
105
- // row the agents never receive is not handed to the model here either.
106
- const content = isSectionObject(section) ? section.content : undefined;
107
- if (typeof content === 'string' && content.trim()) parts.push(content.trim());
108
- }
109
- const text = parts.join('\n\n').trim();
110
- return text || null;
111
- }
112
-
113
- /** A document cut to some of its sections, and the ones left out with their sizes. */
114
- export interface SectionCut {
115
- doc: StoredDocument;
116
- omittedSections: { id: string; characters: number }[];
117
- }
118
-
119
- function cutTo(doc: StoredDocument, sections: Sections, kept: Sections): SectionCut {
120
- const omittedSections = Object.keys(sections)
121
- .filter((id) => !Object.hasOwn(kept, id))
122
- .map((id) => ({ id, characters: sectionContent(sections, id)?.length ?? 0 }));
123
- return { doc: { ...doc, data: { ...(doc.data as DocumentData), sections: kept } }, omittedSections };
124
- }
125
-
126
- /**
127
- * The named sections of a section-shaped document, in the order the API
128
- * returns them (a jsonb column: shorter ids first, then by bytes, not the
129
- * order a person wrote them);
130
- * every other section is listed with its size so the reader knows what else
131
- * there is. Null when the document is not made of sections.
132
- */
133
- export function pickSections(doc: StoredDocument, ids: ReadonlySet<string>): SectionCut | null {
134
- const sections = isRecord(doc.data) ? sectionsOf(doc.data) : null;
135
- if (!sections) return null;
136
- return cutTo(doc, sections, Object.fromEntries(Object.entries(sections).filter(([id]) => ids.has(id))));
137
- }
138
-
139
- /**
140
- * As many whole sections as fit `budget` characters of text, the rest listed
141
- * with their sizes so the reader can ask for them by id. Whole, never cut: a
142
- * section's text is one argument, and half of it reads as all of it. Not "the
143
- * opening": the document's own order is not in the data (a jsonb column
144
- * hands back shorter ids first), so a section that does not fit is skipped
145
- * and a later, smaller one may still be carried. Null when the document is
146
- * not made of sections.
147
- */
148
- export function budgetSections(doc: StoredDocument, budget: number): SectionCut | null {
149
- const sections = isRecord(doc.data) ? sectionsOf(doc.data) : null;
150
- if (!sections) return null;
151
- // Entries, not assignment: a section id is text, and `kept["__proto__"] = x` would set a prototype.
152
- const kept: [string, unknown][] = [];
153
- let used = 0;
154
- for (const id of Object.keys(sections)) {
155
- const length = sectionContent(sections, id)?.length ?? 0;
156
- if (used + length > budget) continue;
157
- kept.push([id, sections[id]]);
158
- used += length;
159
- }
160
- return cutTo(doc, sections, Object.fromEntries(kept));
161
- }
162
-
163
- /**
164
- * The `sections` input of a document read: some sections by id, none for the
165
- * outline (every id with its size and no text), or omitted for the whole.
166
- */
167
- export const sectionsInput = z
168
- .array(z.string().min(1).max(200))
169
- .max(50)
170
- .optional()
171
- .describe(
172
- 'Only these sections, by id; [] for the outline (under outline: every section id and its size, no text); omit for the whole document'
173
- );
174
-
175
- /** The sections input of a company document, where a policy control id also names the sections that state it. */
176
- export const policySectionsInput = sectionsInput.describe(
177
- 'Only these sections, by id, or a policy control id (SEC-5.1) for the sections that state it; [] for the outline (under outline: every section id and its size, no text); omit for the whole document'
178
- );
179
-
180
- /**
181
- * The sections that state each control id asked for (PRO-2800): policy
182
- * standing cites SEC-5.1, and the section holding it has an id of its own
183
- * (secretsManagement) an agent could only guess. A control id is found in
184
- * the sections' text, matched as a whole id so SEC-5.1 is not SEC-5.10.
185
- */
186
- function controlSections(sections: Sections, ids: readonly string[]): Record<string, string[]> {
187
- const found: Record<string, string[]> = {};
188
- for (const id of ids) {
189
- // The API's control vocabulary, overlay families included (TEL-TIC-1.1, HC-FI-DATA-1.1).
190
- if (!isControlId(id) || Object.hasOwn(sections, id)) continue;
191
- // Whole id only: not SEC-5.10, SEC-5.1.2, SEC-5.1A or SEC-5.1-appendix; a sentence's full stop is fine.
192
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
193
- const pattern = new RegExp(`(^|[^A-Za-z0-9.-])${escaped}(?![A-Za-z0-9-]|\\.[A-Za-z0-9])`);
194
- const holders = Object.keys(sections).filter((sectionId) =>
195
- pattern.test(sectionContent(sections, sectionId) ?? '')
196
- );
197
- if (holders.length > 0) found[id] = holders;
198
- }
199
- return found;
200
- }
201
-
202
- /**
203
- * A policy outline names the controls each section states (PRO-2800): a reviewer guessed secretsManagement from its
204
- * name, which works for secrets and not for "what covers logging of secret access". Read off each section's text.
205
- */
206
- function withControls(outline: { id: string; characters: number }[], sections: Sections, policyType: unknown) {
207
- // The first cell of a control table row, as the API reads a policy's controls (policy-controls.ts CONTROL_ROW):
208
- // not AES-256 in prose, nor "see SEC-5.1" in a section that only cites it.
209
- // A reserved row (_Reserved_) is a retired id, not a control, as heldControlMap skips it.
210
- const pattern = new RegExp(`^\\|\\s*(${CONTROL_ID_SOURCE})\\s*\\|\\s*(.*?)\\s*(?:\\||$)`, 'gm');
211
- return outline.map((entry) => {
212
- const text = sectionContent(sections, entry.id) ?? '';
213
- const ids = [
214
- ...new Set(
215
- [...text.matchAll(pattern)]
216
- .filter((match) => !/^_?Reserved_?/i.test(match[2] ?? ''))
217
- .map((match) => match[1]!)
218
- // A base control belongs to the policy its prefix names: an SEC row in the data policy is a stale mention,
219
- // as heldControlMap reads it. An overlay prefix stays where it is written.
220
- .filter((id) => {
221
- const owner = policyTypeForControl(id);
222
- return owner === undefined || owner === policyType;
223
- })
224
- ),
225
- ];
226
- return ids.length > 0 ? { ...entry, controls: ids } : entry;
227
- });
228
- }
229
-
230
- /**
231
- * The answer to a read that names sections: the ones the document has, the
232
- * rest listed with their sizes, and the asked-for ids it lacks under
233
- * unknownSections. One shape whatever matched (PRO-2778): a wrong id does not
234
- * throw away the right ones, and an answer with none right still lists every
235
- * real id with its size, so the retry needs nothing else. No ids is the
236
- * outline.
237
- */
238
- export function sectionReadResult(
239
- doc: StoredDocument,
240
- asked: readonly string[],
241
- label: string,
242
- /** Policies are where control ids are stated; elsewhere one is an unknown id with a pointer. */
243
- options: { controlIds?: boolean } = {}
244
- ): CallToolResult {
245
- const before = sectionsOf(doc.data as DocumentData);
246
- const controls = before && options.controlIds ? controlSections(before, asked) : {};
247
- // A control id reads the sections that state it; the id is then not unknown.
248
- const ids = [...new Set([...asked.filter((id) => !Object.hasOwn(controls, id)), ...Object.values(controls).flat()])];
249
- const cut = pickSections(doc, new Set(ids));
250
- if (!cut) return toolError('VALIDATION_ERROR', `The ${label} is not made of sections; read it without sections.`);
251
- const all = Object.keys(sectionsOf(doc.data as DocumentData) ?? {});
252
- const unknownSections = ids.filter((id) => !all.includes(id));
253
- const known = ids.filter((id) => all.includes(id));
254
- const found = Object.keys(controls).length > 0 ? { controls } : {};
255
- // A control id asked of a document that does not state controls: name the policy that does.
256
- const asControl = options.controlIds ? undefined : unknownSections.find((id) => isControlId(id));
257
- const policy = asControl ? policyNamedByControl(asControl) : undefined;
258
- const controlNote = asControl
259
- ? {
260
- note: policy
261
- ? `${asControl} is a control of ${policy}: read it with taiga_get_document (type: ${policy}, sections: ["${asControl}"]).`
262
- : `${asControl} looks like a policy control id, which only a policy resolves: taiga_get_document without type lists them.`,
263
- }
264
- : {};
265
- if (known.length === 0) {
266
- // The outline, or a read naming only ids the document lacks: no text to
267
- // carry, so no empty data.sections beside the ids, which read as a
268
- // document with no sections (PRO-2800).
269
- const { data: _data, ...meta } = cut.doc;
270
- return toolResult({
271
- ...meta,
272
- outline:
273
- options.controlIds && before
274
- ? withControls(cut.omittedSections, before, (doc as { type?: unknown }).type)
275
- : cut.omittedSections,
276
- ...(unknownSections.length > 0 ? { unknownSections } : {}),
277
- ...controlNote,
278
- });
279
- }
280
- return toolResult({
281
- ...cut.doc,
282
- ...found,
283
- omittedSections: cut.omittedSections,
284
- ...(unknownSections.length > 0 ? { unknownSections } : {}),
285
- ...controlNote,
286
- });
287
- }