@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.
@@ -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;
@@ -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
- constructor(readonly capability: string) {
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.)`;
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