@taiga-ai/mcp-server 0.10.0 → 0.11.0-dev.411
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 +14626 -60
- package/package.json +4 -1
- package/src/client.test.ts +41 -1
- package/src/client.ts +49 -8
- package/src/server.integration.test.ts +127 -6
- package/src/tools/backlogs.ts +39 -8
- package/src/tools/errors.ts +4 -4
- package/src/tools/markdown.test.ts +167 -1
- package/src/tools/markdown.ts +62 -1
- package/src/tools/prompts.ts +4 -1
- package/taiga-ai-mcp-server-0.11.0-dev.411.tgz +0 -0
- package/tsconfig.json +4 -0
- package/taiga-ai-mcp-server-0.10.0.tgz +0 -0
package/src/tools/markdown.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
2
|
+
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
3
|
+
import type { Heading, Node, Parent, PhrasingContent } from 'mdast';
|
|
1
4
|
import { isRecord, sectionContent, sectionsOf } from './sections.js';
|
|
2
5
|
|
|
3
6
|
/**
|
|
@@ -29,7 +32,7 @@ export function renderDocument(doc: Record<string, unknown>): { mimeType: string
|
|
|
29
32
|
for (const id of Object.keys(sections)) {
|
|
30
33
|
const content = sectionContent(sections, id);
|
|
31
34
|
if (content === null) continue;
|
|
32
|
-
lines.push('', `## ${sectionTitle(sections[id], id)}`, '', content.trim());
|
|
35
|
+
lines.push('', `## ${sectionTitle(sections[id], id)}`, '', nestHeadings(content.trim()));
|
|
33
36
|
}
|
|
34
37
|
const { sections: _sections, ...rest } = data;
|
|
35
38
|
const other = Object.fromEntries(Object.entries(rest).filter(([, value]) => !isEmpty(value)));
|
|
@@ -57,6 +60,64 @@ const isEmpty = (value: unknown) =>
|
|
|
57
60
|
(Array.isArray(value) && value.length === 0) ||
|
|
58
61
|
(isRecord(value) && Object.keys(value).length === 0);
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A section body's own headings, one level down, so they sit under the
|
|
65
|
+
* section heading instead of beside it: a derived document writes each
|
|
66
|
+
* section with its own H2. A CommonMark parser says what is a heading, so
|
|
67
|
+
* fenced code, indented code, list items and block quotes are read as a
|
|
68
|
+
* reader's renderer reads them, and a heading-shaped line inside an HTML
|
|
69
|
+
* block is HTML, not a heading, and stays. Every heading lands at least one
|
|
70
|
+
* level under the section's H2, so an H1 and an H2 both become H3 and the
|
|
71
|
+
* rest move one down, H6 at most: an ATX heading (the # kind, an empty one
|
|
72
|
+
* included) takes the marks it needs; a setext heading (underlined)
|
|
73
|
+
* becomes the ATX heading of that depth, serialized from its parsed
|
|
74
|
+
* content (so a quote's prefix, a hard line break or a trailing # run are
|
|
75
|
+
* handled as Markdown, not as text). The edits go by the parser's
|
|
76
|
+
* character offsets, so no line counting of ours can disagree with its own.
|
|
77
|
+
*/
|
|
78
|
+
export function nestHeadings(body: string): string {
|
|
79
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
80
|
+
// An explicit stack, and no spread into push: a body of thousands of
|
|
81
|
+
// nested quotes, or a list of very many items, must not overflow a walk.
|
|
82
|
+
const pending: Node[] = [fromMarkdown(body)];
|
|
83
|
+
while (pending.length > 0) {
|
|
84
|
+
const node = pending.pop()!;
|
|
85
|
+
if (
|
|
86
|
+
node.type === 'heading' &&
|
|
87
|
+
node.position?.start.offset !== undefined &&
|
|
88
|
+
node.position.end.offset !== undefined
|
|
89
|
+
) {
|
|
90
|
+
const start = node.position.start.offset;
|
|
91
|
+
const end = node.position.end.offset;
|
|
92
|
+
const heading = node as Heading;
|
|
93
|
+
const target = nestedDepth(heading.depth);
|
|
94
|
+
const atx = /^#{1,6}(\s|$)/.test(body.slice(start, start + 7));
|
|
95
|
+
if (atx && target > heading.depth) edits.push({ start, end: start, text: '#'.repeat(target - heading.depth) });
|
|
96
|
+
else if (!atx) edits.push({ start, end, text: atxOf(target, heading.children) });
|
|
97
|
+
}
|
|
98
|
+
if ('children' in node) for (const child of (node as Parent).children) pending.push(child);
|
|
99
|
+
}
|
|
100
|
+
let out = body;
|
|
101
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
102
|
+
out = `${out.slice(0, edit.start)}${edit.text}${out.slice(edit.end)}`;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Under the section's H2: at least H3, one level down otherwise, H6 at most. */
|
|
108
|
+
const nestedDepth = (depth: number): Heading['depth'] => Math.min(6, Math.max(3, depth + 1)) as Heading['depth'];
|
|
109
|
+
|
|
110
|
+
/** An ATX heading of this depth from parsed content, its line breaks turned into spaces: an ATX heading is one line. */
|
|
111
|
+
function atxOf(depth: Heading['depth'], children: PhrasingContent[]): string {
|
|
112
|
+
const oneLine = (nodes: PhrasingContent[]): PhrasingContent[] =>
|
|
113
|
+
nodes.map((child) => {
|
|
114
|
+
if (child.type === 'break') return { type: 'text', value: ' ' };
|
|
115
|
+
if (child.type === 'text') return { ...child, value: child.value.replace(/\r\n|\r|\n/g, ' ') };
|
|
116
|
+
return 'children' in child ? { ...child, children: oneLine(child.children as PhrasingContent[]) } : child;
|
|
117
|
+
});
|
|
118
|
+
return toMarkdown({ type: 'heading', depth, children: oneLine(children) }).trimEnd();
|
|
119
|
+
}
|
|
120
|
+
|
|
60
121
|
/** The section's title when it carries one, else its id. */
|
|
61
122
|
function sectionTitle(section: unknown, id: string): string {
|
|
62
123
|
return isRecord(section) && typeof section.title === 'string' && section.title.trim() ? section.title.trim() : id;
|
package/src/tools/prompts.ts
CHANGED
|
@@ -263,8 +263,11 @@ export function registerPrompts(server: McpServer, client: TaigaClient) {
|
|
|
263
263
|
|
|
264
264
|
/** A part the key's capabilities do not cover; named in its place rather than fetched. */
|
|
265
265
|
class NotGranted extends Error {
|
|
266
|
-
|
|
266
|
+
readonly capability: string;
|
|
267
|
+
|
|
268
|
+
constructor(capability: string) {
|
|
267
269
|
super(`needs ${capability}`);
|
|
270
|
+
this.capability = capability;
|
|
268
271
|
}
|
|
269
272
|
line(what: string): string {
|
|
270
273
|
return `(${what} need ${this.capability}, which this key lacks.)`;
|
|
Binary file
|
package/tsconfig.json
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"resolveJsonModule": true,
|
|
12
12
|
"types": ["node"],
|
|
13
13
|
|
|
14
|
+
// @tsconfig/strictest leaves these two out, so every package sets them.
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"erasableSyntaxOnly": true,
|
|
17
|
+
|
|
14
18
|
// Preset flags this package still fails (PRO-2509). Each line is deleted
|
|
15
19
|
// by the issue named beside it; no new line is added here.
|
|
16
20
|
"noPropertyAccessFromIndexSignature": false // PRO-2519
|
|
Binary file
|