@adeu/mcp-server 1.8.0 → 1.9.0

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.
@@ -92,7 +92,20 @@ describe("Cloud Auth & Email Tools MCP Verification", () => {
92
92
  expect(res.result.content[0].text).toContain("Authentication Required");
93
93
  expect(res.result.content[0].text).toContain("login_to_adeu_cloud");
94
94
  });
95
+ it("CLOUD-1b: Enforces authentication trap for list_available_mailboxes", async () => {
96
+ const res = await sendRpc(
97
+ "tools/call",
98
+ {
99
+ name: "list_available_mailboxes",
100
+ arguments: {},
101
+ },
102
+ 204,
103
+ );
95
104
 
105
+ expect(res.result.isError).toBe(true);
106
+ expect(res.result.content[0].text).toContain("Authentication Required");
107
+ expect(res.result.content[0].text).toContain("login_to_adeu_cloud");
108
+ });
96
109
  it("CLOUD-2: Validates create_email_draft missing required arguments", async () => {
97
110
  // To bypass the auth trap for this test, we inject a dummy credential file
98
111
  mkdirSync(adeuConfigDir, { recursive: true });
@@ -1,39 +1,48 @@
1
- import { resolve, basename } from 'node:path';
2
- import {
3
- DocumentObject,
4
- paginate,
5
- split_structural_appendix,
6
- extract_outline,
7
- OutlineNode
8
- } from '@adeu/core';
1
+ import { resolve, basename } from "node:path";
2
+ import {
3
+ DocumentObject,
4
+ paginate,
5
+ split_structural_appendix,
6
+ extract_outline,
7
+ OutlineNode,
8
+ } from "@adeu/core";
9
9
 
10
10
  export interface ToolResult {
11
- content: { type: 'text'; text: string }[];
11
+ content: { type: "text"; text: string }[];
12
+ structuredContent?: any;
12
13
  isError?: boolean;
13
14
  [key: string]: unknown;
14
15
  }
15
16
 
16
17
  function _build_appendix_pointer(has_appendix: boolean): string {
17
- if (!has_appendix) return '';
18
+ if (!has_appendix) return "";
18
19
  return `\n\n---\n\n> **Appendix available.** This document has structural metadata (defined terms, cross-references, bookmarks, diagnostics) that may be relevant when editing. Call \`read_docx\` with \`mode='appendix'\` to load it before submitting edits.`;
19
20
  }
20
21
 
21
22
  function _build_page_banner(page: number, total: number): string {
22
- if (total <= 1) return '';
23
+ if (total <= 1) return "";
23
24
  return `> **Page ${page} of ${total}** — call \`read_docx\` with \`mode='outline'\` for a heading map of the full document.\n\n---\n\n`;
24
25
  }
25
26
 
26
- function _build_page_footer(page: number, total: number, has_next: boolean): string {
27
- if (total <= 1 || !has_next) return '';
27
+ function _build_page_footer(
28
+ page: number,
29
+ total: number,
30
+ has_next: boolean,
31
+ ): string {
32
+ if (total <= 1 || !has_next) return "";
28
33
  return `\n\n---\n\n> **Continues on page ${page + 1} of ${total}.**`;
29
34
  }
30
35
 
31
- export function render_outline_tree(nodes: OutlineNode[], max_level: number = 2, verbose: boolean = false): string {
36
+ export function render_outline_tree(
37
+ nodes: OutlineNode[],
38
+ max_level: number = 2,
39
+ verbose: boolean = false,
40
+ ): string {
32
41
  if (!nodes || nodes.length === 0) {
33
- return '# (No headings detected)\n\nThis document has no detectable headings.';
42
+ return "# (No headings detected)\n\nThis document has no detectable headings.";
34
43
  }
35
44
 
36
- const visible = nodes.filter(n => n.level <= max_level);
45
+ const visible = nodes.filter((n) => n.level <= max_level);
37
46
 
38
47
  if (visible.length === 0) {
39
48
  return `# (No headings at level <= ${max_level})\n\nDocument has ${nodes.length} headings, all at deeper levels. Call read_docx with mode='outline' and outline_max_level=N (up to 6) to see them.`;
@@ -41,107 +50,159 @@ export function render_outline_tree(nodes: OutlineNode[], max_level: number = 2,
41
50
 
42
51
  const lines: string[] = [];
43
52
  for (const node of visible) {
44
- const prefix = '#'.repeat(node.level);
53
+ const prefix = "#".repeat(node.level);
45
54
  if (verbose) {
46
55
  const meta_parts = [`p${node.page}`, node.style];
47
- if (node.has_table) meta_parts.push('has table');
48
- if (node.footnote_ids && node.footnote_ids.length > 0) meta_parts.push('fn:' + node.footnote_ids.join(','));
49
- lines.push(`${prefix} ${node.text} (${meta_parts.join(', ')})`);
56
+ if (node.has_table) meta_parts.push("has table");
57
+ if (node.footnote_ids && node.footnote_ids.length > 0)
58
+ meta_parts.push("fn:" + node.footnote_ids.join(","));
59
+ lines.push(`${prefix} ${node.text} (${meta_parts.join(", ")})`);
50
60
  } else {
51
61
  lines.push(`${prefix} ${node.text} (p${node.page})`);
52
62
  }
53
63
  }
54
- return lines.join('\n');
64
+ return lines.join("\n");
55
65
  }
56
66
 
57
- export function build_paginated_response(text: string, page: number, file_path: string): ToolResult {
67
+ export function build_paginated_response(
68
+ text: string,
69
+ page: number,
70
+ file_path: string,
71
+ ): ToolResult {
58
72
  const [body, appendix] = split_structural_appendix(text);
59
73
  const has_appendix = Boolean(appendix.trim());
60
74
 
61
- const result = paginate(body, '');
75
+ const result = paginate(body, "");
62
76
 
63
77
  if (page < 1 || page > result.total_pages) {
64
- throw new Error(`Page ${page} out of range (doc has ${result.total_pages} pages).`);
78
+ throw new Error(
79
+ `Page ${page} out of range (doc has ${result.total_pages} pages).`,
80
+ );
65
81
  }
66
82
 
67
83
  const selected = result.pages[page - 1];
68
84
  const banner = _build_page_banner(selected.page, selected.total_pages);
69
- const footer = _build_page_footer(selected.page, selected.total_pages, selected.has_next);
85
+ const footer = _build_page_footer(
86
+ selected.page,
87
+ selected.total_pages,
88
+ selected.has_next,
89
+ );
70
90
  const appendix_pointer = _build_appendix_pointer(has_appendix);
71
-
72
- const ui_markdown = banner + selected.page_content + footer + appendix_pointer;
91
+
92
+ const ui_markdown =
93
+ banner + selected.page_content + footer + appendix_pointer;
73
94
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
74
95
 
75
96
  return {
76
- content: [{ type: 'text', text: llm_content }]
97
+ content: [{ type: "text", text: llm_content }],
98
+ // Include structuredContent for the UI to render the markdown
99
+ structuredContent: {
100
+ markdown: ui_markdown,
101
+ file_path: resolve(file_path),
102
+ title: basename(file_path),
103
+ },
77
104
  };
78
105
  }
79
106
 
80
107
  export function build_outline_response(
81
- doc: DocumentObject,
82
- projected_text: string,
83
- file_path: string,
84
- outline_max_level: number = 2,
85
- outline_verbose: boolean = false
108
+ doc: DocumentObject,
109
+ projected_text: string,
110
+ file_path: string,
111
+ outline_max_level: number = 2,
112
+ outline_verbose: boolean = false,
86
113
  ): ToolResult {
87
114
  const [body] = split_structural_appendix(projected_text);
88
- const pagination_result = paginate(body, '');
115
+ const pagination_result = paginate(body, "");
89
116
 
90
117
  const nodes = extract_outline(
91
118
  doc,
92
119
  body,
93
120
  pagination_result.body_pages,
94
- pagination_result.body_page_offsets
121
+ pagination_result.body_page_offsets,
95
122
  );
96
123
 
97
- const rendered = render_outline_tree(nodes, outline_max_level, outline_verbose);
124
+ const rendered = render_outline_tree(
125
+ nodes,
126
+ outline_max_level,
127
+ outline_verbose,
128
+ );
98
129
 
99
- const visible_count = nodes.filter(n => n.level <= outline_max_level).length;
130
+ const visible_count = nodes.filter(
131
+ (n) => n.level <= outline_max_level,
132
+ ).length;
100
133
  const deeper_count = nodes.length - visible_count;
101
- const deeper_hint = deeper_count > 0 ? ` (${deeper_count} more at deeper levels, raise outline_max_level to see)` : '';
134
+ const deeper_hint =
135
+ deeper_count > 0
136
+ ? ` (${deeper_count} more at deeper levels, raise outline_max_level to see)`
137
+ : "";
102
138
 
103
139
  const header = `> **Outline view** — showing ${visible_count} of ${nodes.length} headings (L1-L${outline_max_level}${deeper_hint}) across ${pagination_result.total_pages} page(s). Call \`read_docx\` with \`mode='full'\` and \`page=N\` to read a section.\n\n---\n\n`;
104
140
  const ui_markdown = header + rendered;
105
141
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
106
142
 
107
143
  return {
108
- content: [{ type: 'text', text: llm_content }]
144
+ content: [{ type: "text", text: llm_content }],
145
+ structuredContent: {
146
+ markdown: ui_markdown,
147
+ file_path: resolve(file_path),
148
+ title: `Outline: ${basename(file_path)}`,
149
+ },
109
150
  };
110
151
  }
111
152
 
112
- export function build_appendix_response(text: string, page: number, file_path: string): ToolResult {
153
+ export function build_appendix_response(
154
+ text: string,
155
+ page: number,
156
+ file_path: string,
157
+ ): ToolResult {
113
158
  const [, appendix] = split_structural_appendix(text);
114
159
 
115
160
  if (!appendix.trim()) {
116
- const ui_markdown = "# Appendix\n\nThis document has no structural appendix (no defined terms, named anchors, or diagnostics detected).";
161
+ const ui_markdown =
162
+ "# Appendix\n\nThis document has no structural appendix (no defined terms, named anchors, or diagnostics detected).";
117
163
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
118
164
  return {
119
- content: [{ type: 'text', text: llm_content }]
165
+ content: [{ type: "text", text: llm_content }],
166
+ structuredContent: {
167
+ markdown: ui_markdown,
168
+ file_path: resolve(file_path),
169
+ title: `Appendix: ${basename(file_path)}`,
170
+ },
120
171
  };
121
172
  }
122
173
 
123
- const result = paginate(appendix, '');
174
+ const result = paginate(appendix, "");
124
175
 
125
176
  if (page < 1 || page > result.total_pages) {
126
- throw new Error(`Appendix page ${page} out of range (appendix has ${result.total_pages} pages).`);
177
+ throw new Error(
178
+ `Appendix page ${page} out of range (appendix has ${result.total_pages} pages).`,
179
+ );
127
180
  }
128
181
 
129
182
  const selected = result.pages[page - 1];
130
183
 
131
- let banner = '';
132
- let footer = '';
184
+ let banner = "";
185
+ let footer = "";
133
186
 
134
187
  if (selected.total_pages > 1) {
135
188
  banner = `> **Appendix page ${selected.page} of ${selected.total_pages}** — structural metadata for this document.\n\n---\n\n`;
136
- footer = selected.has_next ? `\n\n---\n\n> **Continues on appendix page ${selected.page + 1} of ${selected.total_pages}.**` : '';
189
+ footer = selected.has_next
190
+ ? `\n\n---\n\n> **Continues on appendix page ${selected.page + 1} of ${selected.total_pages}.**`
191
+ : "";
137
192
  } else {
138
- banner = "> **Appendix** — structural metadata for this document.\n\n---\n\n";
193
+ banner =
194
+ "> **Appendix** — structural metadata for this document.\n\n---\n\n";
139
195
  }
140
196
 
141
197
  const ui_markdown = banner + selected.page_content + footer;
142
198
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
143
199
 
144
200
  return {
145
- content: [{ type: 'text', text: llm_content }]
201
+ content: [{ type: "text", text: llm_content }],
202
+ structuredContent: {
203
+ markdown: ui_markdown,
204
+ file_path: resolve(file_path),
205
+ title: `Appendix: ${basename(file_path)}`,
206
+ },
146
207
  };
147
- }
208
+ }