@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.
- package/dist/assets/adeu.svg +3 -0
- package/dist/assets/logo.png +0 -0
- package/dist/assets/marked.min.js +69 -0
- package/dist/index.js +523 -395
- package/dist/index.js.map +1 -1
- package/dist/templates/email_ui.html +667 -0
- package/dist/templates/markdown_ui.html +745 -0
- package/package.json +4 -2
- package/src/assets/adeu.svg +3 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/marked.min.js +69 -0
- package/src/index.ts +510 -407
- package/src/mcp.cloud.test.ts +13 -0
- package/src/response-builders.ts +111 -50
- package/src/templates/email_ui.html +667 -0
- package/src/templates/markdown_ui.html +745 -0
- package/src/tools/email.ts +61 -2
- package/tsup.config.ts +35 -11
package/src/mcp.cloud.test.ts
CHANGED
|
@@ -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 });
|
package/src/response-builders.ts
CHANGED
|
@@ -1,39 +1,48 @@
|
|
|
1
|
-
import { resolve, basename } from
|
|
2
|
-
import {
|
|
3
|
-
DocumentObject,
|
|
4
|
-
paginate,
|
|
5
|
-
split_structural_appendix,
|
|
6
|
-
extract_outline,
|
|
7
|
-
OutlineNode
|
|
8
|
-
} from
|
|
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:
|
|
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(
|
|
27
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
48
|
-
if (node.footnote_ids && node.footnote_ids.length > 0)
|
|
49
|
-
|
|
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(
|
|
64
|
+
return lines.join("\n");
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
export function build_paginated_response(
|
|
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(
|
|
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(
|
|
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 =
|
|
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:
|
|
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(
|
|
124
|
+
const rendered = render_outline_tree(
|
|
125
|
+
nodes,
|
|
126
|
+
outline_max_level,
|
|
127
|
+
outline_verbose,
|
|
128
|
+
);
|
|
98
129
|
|
|
99
|
-
const visible_count = nodes.filter(
|
|
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 =
|
|
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:
|
|
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(
|
|
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 =
|
|
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:
|
|
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(
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
+
}
|