tavant-docs-mcp 1.0.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/LICENSE +21 -0
- package/assets/bg-agenda-data.jpeg +0 -0
- package/assets/bg-breaker-brain.jpeg +0 -0
- package/assets/bg-breaker-cloud.jpeg +0 -0
- package/assets/bg-breaker-lines.jpeg +0 -0
- package/assets/bg-thankyou.jpeg +0 -0
- package/assets/bg-title-tech.jpeg +0 -0
- package/assets/cr-image1.png +0 -0
- package/assets/decor-cubes.png +0 -0
- package/assets/footer-bar.png +0 -0
- package/assets/tavant-logo-orange.png +0 -0
- package/assets/tavant-logo-small.png +0 -0
- package/assets/tavant-logo-white-sm.png +0 -0
- package/assets/tavant-logo-white.png +0 -0
- package/assets/tavant-template.potx +0 -0
- package/brand.js +21 -0
- package/index.js +172 -0
- package/knowledge/tavant-company.md +181 -0
- package/knowledge/tavant-template.md +61 -0
- package/package.json +32 -0
- package/templates/contract/builders.js +317 -0
- package/templates/contract/register.js +213 -0
- package/templates/contract/sections.js +73 -0
- package/templates/cr/builders.js +286 -0
- package/templates/cr/register.js +189 -0
- package/templates/cr/sections.js +55 -0
- package/templates/msa/builders.js +480 -0
- package/templates/msa/register.js +185 -0
- package/templates/msa/sections.js +86 -0
- package/templates/nda/builders.js +277 -0
- package/templates/nda/register.js +185 -0
- package/templates/nda/sections.js +73 -0
- package/templates/pptx/builders.js +712 -0
- package/templates/pptx/layouts.js +168 -0
- package/templates/pptx/register.js +363 -0
- package/templates/sow/builders.js +294 -0
- package/templates/sow/register.js +183 -0
- package/templates/sow/sections.js +76 -0
- package/test-custom-slide.js +79 -0
- package/test-e2e.js +190 -0
- package/test-msa.js +48 -0
- package/test-nda-cr.js +88 -0
- package/test-pptx.js +93 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Layout definitions matching the real Tavant Corporate Template 2025
|
|
2
|
+
// 23 layouts extracted from the .potx file
|
|
3
|
+
|
|
4
|
+
const LAYOUTS = {
|
|
5
|
+
title_cover: {
|
|
6
|
+
id: "title_cover",
|
|
7
|
+
name: "Title Slide",
|
|
8
|
+
layoutRef: "Layout 1: Title Slide_B",
|
|
9
|
+
description: "Dark background with tech imagery, orange Tavant logo, large title (up to 3 lines), optional subtitle and date. Use as the FIRST slide.",
|
|
10
|
+
fields: ["title", "subtitle", "date"],
|
|
11
|
+
},
|
|
12
|
+
agenda: {
|
|
13
|
+
id: "agenda",
|
|
14
|
+
name: "Agenda",
|
|
15
|
+
layoutRef: "Layout 2: Agenda",
|
|
16
|
+
description: "Dark data-waves background, orange numbered talking points (up to 6 items), decorative cubes corner element.",
|
|
17
|
+
fields: ["items"],
|
|
18
|
+
},
|
|
19
|
+
breaker_ai: {
|
|
20
|
+
id: "breaker_ai",
|
|
21
|
+
name: "Breaker — AI",
|
|
22
|
+
layoutRef: "Layout 3: BREAKER SLIDE 2",
|
|
23
|
+
description: "Full-bleed AI brain imagery, bold orange title, optional key points. Use to introduce new sections.",
|
|
24
|
+
fields: ["title", "key_points"],
|
|
25
|
+
},
|
|
26
|
+
breaker_cloud: {
|
|
27
|
+
id: "breaker_cloud",
|
|
28
|
+
name: "Breaker — Cloud",
|
|
29
|
+
layoutRef: "Layout 4: BREAKER SLIDE 3",
|
|
30
|
+
description: "Full-bleed cloud/abstract imagery, bold orange title. Section divider with cloud/data theme.",
|
|
31
|
+
fields: ["title", "key_points"],
|
|
32
|
+
},
|
|
33
|
+
breaker_abstract: {
|
|
34
|
+
id: "breaker_abstract",
|
|
35
|
+
name: "Breaker — Abstract",
|
|
36
|
+
layoutRef: "Layout 5: BREAKER SLIDE 4",
|
|
37
|
+
description: "Full-bleed abstract lines imagery, bold orange title. Section divider with abstract/tech theme.",
|
|
38
|
+
fields: ["title", "key_points"],
|
|
39
|
+
},
|
|
40
|
+
blank: {
|
|
41
|
+
id: "blank",
|
|
42
|
+
name: "Blank",
|
|
43
|
+
layoutRef: "Layout 6: Blank",
|
|
44
|
+
description: "White slide with only footer bar and logo. For custom content.",
|
|
45
|
+
fields: [],
|
|
46
|
+
},
|
|
47
|
+
title_only: {
|
|
48
|
+
id: "title_only",
|
|
49
|
+
name: "Title Only",
|
|
50
|
+
layoutRef: "Layout 7: Title Only",
|
|
51
|
+
description: "White slide with just a title at top. Open area for custom content below.",
|
|
52
|
+
fields: ["title"],
|
|
53
|
+
},
|
|
54
|
+
title_only_dark: {
|
|
55
|
+
id: "title_only_dark",
|
|
56
|
+
name: "Title Only (Dark)",
|
|
57
|
+
layoutRef: "Layout 8: Title Only - Grey",
|
|
58
|
+
description: "Dark grey (#222222) slide with title at top. Open area for custom content below.",
|
|
59
|
+
fields: ["title"],
|
|
60
|
+
},
|
|
61
|
+
content_dark: {
|
|
62
|
+
id: "content_dark",
|
|
63
|
+
name: "Title + Content (Dark)",
|
|
64
|
+
layoutRef: "Layout 9: Title + Content - Grey",
|
|
65
|
+
description: "Dark grey slide with title and content area. For detailed text on dark background.",
|
|
66
|
+
fields: ["title", "body"],
|
|
67
|
+
},
|
|
68
|
+
content: {
|
|
69
|
+
id: "content",
|
|
70
|
+
name: "Title + Content",
|
|
71
|
+
layoutRef: "Layout 10: Title + Content",
|
|
72
|
+
description: "White slide with title and large content area. General-purpose content slide.",
|
|
73
|
+
fields: ["title", "body"],
|
|
74
|
+
},
|
|
75
|
+
title_subtitle: {
|
|
76
|
+
id: "title_subtitle",
|
|
77
|
+
name: "Title + Subtitle",
|
|
78
|
+
layoutRef: "Layout 11: Title + Subtitle",
|
|
79
|
+
description: "White slide with title (24pt bold) and orange subtitle (18pt). For section introductions.",
|
|
80
|
+
fields: ["title", "subtitle"],
|
|
81
|
+
},
|
|
82
|
+
two_column: {
|
|
83
|
+
id: "two_column",
|
|
84
|
+
name: "Title + 2-Column Content",
|
|
85
|
+
layoutRef: "Layout 12: Title + 2-Column Content",
|
|
86
|
+
description: "White slide with title, subtitle, and two equal content columns. Good for comparisons.",
|
|
87
|
+
fields: ["title", "subtitle", "left_content", "right_content"],
|
|
88
|
+
},
|
|
89
|
+
title_subtitle_content: {
|
|
90
|
+
id: "title_subtitle_content",
|
|
91
|
+
name: "Title + Subtitle + Content",
|
|
92
|
+
layoutRef: "Layout 13: Title + Subtitle + Content",
|
|
93
|
+
description: "White slide with title, orange subtitle, and full-width content area below. Most versatile layout.",
|
|
94
|
+
fields: ["title", "subtitle", "body"],
|
|
95
|
+
},
|
|
96
|
+
multi_case_study: {
|
|
97
|
+
id: "multi_case_study",
|
|
98
|
+
name: "Multi-Case Study (4 columns)",
|
|
99
|
+
layoutRef: "Layout 16: Multi-Case Study",
|
|
100
|
+
description: "Black background, 4 equal columns with image placeholders and text. For showcasing 4 case studies or capabilities.",
|
|
101
|
+
fields: ["title", "subtitle", "columns"],
|
|
102
|
+
},
|
|
103
|
+
image_content_a: {
|
|
104
|
+
id: "image_content_a",
|
|
105
|
+
name: "Image + Content A",
|
|
106
|
+
layoutRef: "Layout 17: Image + Content A",
|
|
107
|
+
description: "White slide with content on left and large image placeholder on right. For case studies.",
|
|
108
|
+
fields: ["title", "subtitle", "body", "image_description"],
|
|
109
|
+
},
|
|
110
|
+
image_content_b: {
|
|
111
|
+
id: "image_content_b",
|
|
112
|
+
name: "Image + Content B",
|
|
113
|
+
layoutRef: "Layout 15: Image + Content B",
|
|
114
|
+
description: "Black background with image left, 2 topic boxes right, and content area below. For detailed analysis.",
|
|
115
|
+
fields: ["title", "subtitle", "topic_1", "topic_2", "body"],
|
|
116
|
+
},
|
|
117
|
+
image_grid: {
|
|
118
|
+
id: "image_grid",
|
|
119
|
+
name: "Image + Content Grid",
|
|
120
|
+
layoutRef: "Layout 18: Image + Content C",
|
|
121
|
+
description: "Photo/image top, 2x3 grid of text blocks below. Title overlaid on image. For capabilities overview.",
|
|
122
|
+
fields: ["title", "subtitle", "grid_items"],
|
|
123
|
+
},
|
|
124
|
+
three_column_images: {
|
|
125
|
+
id: "three_column_images",
|
|
126
|
+
name: "3-Column with Images",
|
|
127
|
+
layoutRef: "Layout 19: Images + Content D",
|
|
128
|
+
description: "Black background, 3 image slots top, 3 numbered topic blocks below. For services/features.",
|
|
129
|
+
fields: ["title", "subtitle", "columns"],
|
|
130
|
+
},
|
|
131
|
+
chart: {
|
|
132
|
+
id: "chart",
|
|
133
|
+
name: "Content + Chart",
|
|
134
|
+
layoutRef: "Layout 20: Content + Chart",
|
|
135
|
+
description: "Grey (#77787B) background, content left with key takeaway box, chart right. For data-driven slides.",
|
|
136
|
+
fields: ["title", "subtitle", "body", "takeaway", "chart_data"],
|
|
137
|
+
},
|
|
138
|
+
timeline_vertical: {
|
|
139
|
+
id: "timeline_vertical",
|
|
140
|
+
name: "Timeline (Vertical)",
|
|
141
|
+
layoutRef: "Layout 21: Timeline 1",
|
|
142
|
+
description: "Black background, vertical year timeline on right, 3 stat/content blocks, descriptive text. For KPIs over time.",
|
|
143
|
+
fields: ["title", "subtitle", "body", "blocks", "year_highlight"],
|
|
144
|
+
},
|
|
145
|
+
timeline_horizontal: {
|
|
146
|
+
id: "timeline_horizontal",
|
|
147
|
+
name: "Timeline (Horizontal)",
|
|
148
|
+
layoutRef: "Layout 22: Timeline 2",
|
|
149
|
+
description: "Orange (#F26F26) background, horizontal timeline with 8 date points and alternating content blocks. For roadmaps.",
|
|
150
|
+
fields: ["title", "subtitle", "milestones"],
|
|
151
|
+
},
|
|
152
|
+
multi_quote: {
|
|
153
|
+
id: "multi_quote",
|
|
154
|
+
name: "Multi-Quote / Testimonials",
|
|
155
|
+
layoutRef: "Layout 23: Multi-Quote",
|
|
156
|
+
description: "Black background, 3 rows with logo/image slots left and quote text right. For client testimonials.",
|
|
157
|
+
fields: ["title", "subtitle", "quotes"],
|
|
158
|
+
},
|
|
159
|
+
thank_you: {
|
|
160
|
+
id: "thank_you",
|
|
161
|
+
name: "Thank You",
|
|
162
|
+
layoutRef: "Layout 14: Thank You",
|
|
163
|
+
description: "Dark background with abstract imagery, large THANK YOU text, office locations, contact info. Use as the LAST slide.",
|
|
164
|
+
fields: ["contact_email", "contact_website", "contact_phone"],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
module.exports = LAYOUTS;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
const PptxGenJS = require("pptxgenjs");
|
|
2
|
+
const { z } = require("zod");
|
|
3
|
+
const { v4: uuidv4 } = require("uuid");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const LAYOUTS = require("./layouts");
|
|
7
|
+
const slideBuilders = require("./builders");
|
|
8
|
+
const BRAND = require("../../brand");
|
|
9
|
+
|
|
10
|
+
const ASSETS = path.join(__dirname, "..", "..", "assets");
|
|
11
|
+
const imgPath = (name) => path.join(ASSETS, name);
|
|
12
|
+
|
|
13
|
+
const presentations = new Map();
|
|
14
|
+
|
|
15
|
+
// ─── Helpers for creative slide building ────────────────────────────────
|
|
16
|
+
function addChromeToSlide(slide, isDark) {
|
|
17
|
+
// Footer bar (black bar with orange triangle)
|
|
18
|
+
if (fs.existsSync(imgPath("footer-bar.png"))) {
|
|
19
|
+
slide.addImage({ path: imgPath("footer-bar.png"), x: 0, y: 6.83, w: 13.33, h: 0.67 });
|
|
20
|
+
}
|
|
21
|
+
// Tavant logo — LEFT bottom on footer bar
|
|
22
|
+
if (fs.existsSync(imgPath("tavant-logo-small.png"))) {
|
|
23
|
+
slide.addImage({ path: imgPath("tavant-logo-small.png"), x: 0.29, y: 7.07, w: 1.38, h: 0.38 });
|
|
24
|
+
}
|
|
25
|
+
// Confidential text — right bottom, black, single line
|
|
26
|
+
slide.addText("Tavant & Customer Confidential", {
|
|
27
|
+
x: 9.5, y: 7.15, w: 3.0, h: 0.18,
|
|
28
|
+
fontSize: 10.7, color: "000000", fontFace: BRAND.font, wrap: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function register(server) {
|
|
33
|
+
|
|
34
|
+
// ─── Tool: list layouts ────────────────────────────────────────────────
|
|
35
|
+
server.tool(
|
|
36
|
+
"pptx_list_layouts",
|
|
37
|
+
"List all available Tavant corporate slide layouts with their descriptions and required fields",
|
|
38
|
+
{},
|
|
39
|
+
async () => ({
|
|
40
|
+
content: [{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: JSON.stringify(Object.values(LAYOUTS).map((l) => ({
|
|
43
|
+
id: l.id, name: l.name, description: l.description, fields: l.fields,
|
|
44
|
+
})), null, 2),
|
|
45
|
+
}],
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ─── Tool: create presentation ─────────────────────────────────────────
|
|
50
|
+
server.tool(
|
|
51
|
+
"pptx_create",
|
|
52
|
+
"Create a new empty Tavant-branded PowerPoint presentation. Returns a presentation_id.",
|
|
53
|
+
{
|
|
54
|
+
title: z.string().optional().describe("Presentation title"),
|
|
55
|
+
author: z.string().optional().describe("Author name"),
|
|
56
|
+
},
|
|
57
|
+
async ({ title, author }) => {
|
|
58
|
+
const id = uuidv4();
|
|
59
|
+
const pptx = new PptxGenJS();
|
|
60
|
+
pptx.layout = "LAYOUT_WIDE"; // 13.33 x 7.50
|
|
61
|
+
pptx.title = title || "Tavant Presentation";
|
|
62
|
+
pptx.author = author || "Tavant";
|
|
63
|
+
pptx.company = "Tavant";
|
|
64
|
+
presentations.set(id, { pptx, slideCount: 0, title: pptx.title });
|
|
65
|
+
return {
|
|
66
|
+
content: [{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: JSON.stringify({
|
|
69
|
+
presentation_id: id,
|
|
70
|
+
title: pptx.title,
|
|
71
|
+
slide_size: "13.33 x 7.50 inches (widescreen)",
|
|
72
|
+
body_area: "x:0.36 y:1.20 to x:12.97 y:6.70 (usable body after title, before footer)",
|
|
73
|
+
message: "Presentation created. Use pptx_add_slide for template layouts, or pptx_add_custom_slide + pptx_add_element for creative freedom. Use pptx_export to save.",
|
|
74
|
+
}),
|
|
75
|
+
}],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ─── Tool: add slide (template layout) ─────────────────────────────────
|
|
81
|
+
server.tool(
|
|
82
|
+
"pptx_add_slide",
|
|
83
|
+
"Add a slide using a pre-built Tavant corporate template layout. Good for standard slides. For creative/custom layouts, use pptx_add_custom_slide + pptx_add_element instead.",
|
|
84
|
+
{
|
|
85
|
+
presentation_id: z.string().describe("The presentation ID"),
|
|
86
|
+
layout: z.string().describe(
|
|
87
|
+
"Layout ID: title_cover, agenda, breaker_ai, breaker_cloud, breaker_abstract, blank, title_only, title_only_dark, content_dark, content, title_subtitle, two_column, title_subtitle_content, multi_case_study, image_content_a, image_content_b, image_grid, three_column_images, chart, timeline_vertical, timeline_horizontal, multi_quote, thank_you"
|
|
88
|
+
),
|
|
89
|
+
data: z.record(z.any()).describe(
|
|
90
|
+
"Slide content — fields depend on layout. Use pptx_list_layouts to see fields. body can be string or string[]. columns/grid_items: [{title,description}]. milestones: [{date,label}]. chart_data: {labels:[],values:[]}. quotes: [{company,title,text}]."
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
async ({ presentation_id, layout, data }) => {
|
|
94
|
+
const pres = presentations.get(presentation_id);
|
|
95
|
+
if (!pres) return { content: [{ type: "text", text: "Error: Presentation not found." }], isError: true };
|
|
96
|
+
const builder = slideBuilders[layout];
|
|
97
|
+
if (!builder) {
|
|
98
|
+
return { content: [{ type: "text", text: `Error: Unknown layout "${layout}". Available: ${Object.keys(LAYOUTS).join(", ")}` }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
builder(pres.pptx, data || {});
|
|
101
|
+
pres.slideCount++;
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: JSON.stringify({ message: `Slide added (${layout})`, slide_number: pres.slideCount, total_slides: pres.slideCount }) }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// ─── Tool: add CUSTOM slide (chrome only — body is yours) ──────────────
|
|
109
|
+
server.tool(
|
|
110
|
+
"pptx_add_custom_slide",
|
|
111
|
+
"Add a blank Tavant-branded slide with ONLY the corporate chrome (footer bar, Tavant logo at left-bottom, confidential text). The body area is completely free for you to design creatively using pptx_add_element. Use this when you want to go beyond the standard template layouts — create unique visuals, custom grids, icon layouts, etc. Returns a slide_index to use with pptx_add_element.",
|
|
112
|
+
{
|
|
113
|
+
presentation_id: z.string().describe("The presentation ID"),
|
|
114
|
+
background: z.string().optional().describe("Background color hex without #. Default: FFFFFF. Use 000000 for dark, 222222 for dark grey, F26F26 for orange, 77787B for grey."),
|
|
115
|
+
title: z.string().optional().describe("Optional slide title at standard position (0.36, 0.37)"),
|
|
116
|
+
subtitle: z.string().optional().describe("Optional orange subtitle at standard position (0.36, 0.78)"),
|
|
117
|
+
background_image: z.string().optional().describe("Background image name from assets: bg-title-tech.jpeg, bg-agenda-data.jpeg, bg-breaker-brain.jpeg, bg-breaker-cloud.jpeg, bg-breaker-lines.jpeg, bg-thankyou.jpeg"),
|
|
118
|
+
},
|
|
119
|
+
async ({ presentation_id, background, title, subtitle, background_image }) => {
|
|
120
|
+
const pres = presentations.get(presentation_id);
|
|
121
|
+
if (!pres) return { content: [{ type: "text", text: "Error: Presentation not found." }], isError: true };
|
|
122
|
+
|
|
123
|
+
const bgColor = background || "FFFFFF";
|
|
124
|
+
const isDark = ["000000", "222222", "1A1A1A"].includes(bgColor);
|
|
125
|
+
const slide = pres.pptx.addSlide();
|
|
126
|
+
slide.background = { color: bgColor };
|
|
127
|
+
|
|
128
|
+
// Background image if requested
|
|
129
|
+
if (background_image && fs.existsSync(imgPath(background_image))) {
|
|
130
|
+
slide.addImage({ path: imgPath(background_image), x: 0, y: 0, w: 13.33, h: 7.50 });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Title
|
|
134
|
+
if (title) {
|
|
135
|
+
slide.addText(title, {
|
|
136
|
+
x: 0.36, y: 0.37, w: 12.62, h: 0.39,
|
|
137
|
+
fontSize: 24, bold: true, color: isDark ? "FFFFFF" : "000000", fontFace: BRAND.font,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Subtitle
|
|
142
|
+
if (subtitle) {
|
|
143
|
+
slide.addText(subtitle, {
|
|
144
|
+
x: 0.36, y: 0.78, w: 12.62, h: 0.41,
|
|
145
|
+
fontSize: 18, color: "F77A33", fontFace: BRAND.font,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Corporate chrome (footer, logo, confidential)
|
|
150
|
+
addChromeToSlide(slide, isDark);
|
|
151
|
+
|
|
152
|
+
pres.slideCount++;
|
|
153
|
+
// Store the slide reference for adding elements
|
|
154
|
+
if (!pres.slides) pres.slides = {};
|
|
155
|
+
pres.slides[pres.slideCount] = slide;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: JSON.stringify({
|
|
161
|
+
message: "Custom slide created with Tavant chrome",
|
|
162
|
+
slide_index: pres.slideCount,
|
|
163
|
+
total_slides: pres.slideCount,
|
|
164
|
+
body_area: {
|
|
165
|
+
note: "Add elements freely in this area. Footer starts at y=6.83.",
|
|
166
|
+
x_min: 0.3, y_min: title ? (subtitle ? 1.30 : 0.90) : 0.20,
|
|
167
|
+
x_max: 13.0, y_max: 6.70,
|
|
168
|
+
width: 12.7, height: title ? (subtitle ? 5.40 : 5.80) : 6.50,
|
|
169
|
+
},
|
|
170
|
+
brand_colors: {
|
|
171
|
+
orange: "F36E26", accent_orange: "F77A33", bullet_orange: "FF8909",
|
|
172
|
+
black: "000000", dark_grey: "222222", grey: "77787B",
|
|
173
|
+
white: "FFFFFF", light_grey: "F5F5F5", medium_grey: "666666",
|
|
174
|
+
},
|
|
175
|
+
font: "Aptos",
|
|
176
|
+
}),
|
|
177
|
+
}],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// ─── Tool: add element to a custom slide ───────────────────────────────
|
|
183
|
+
server.tool(
|
|
184
|
+
"pptx_add_element",
|
|
185
|
+
"Add a creative element (text box, shape, chart, table) to a custom slide. Use this after pptx_add_custom_slide to build visually rich, creative slides. You can add multiple elements per slide. Be creative — use colored boxes, icon-style numbers, accent shapes, multi-column text, etc.",
|
|
186
|
+
{
|
|
187
|
+
presentation_id: z.string().describe("The presentation ID"),
|
|
188
|
+
slide_index: z.number().describe("The slide_index from pptx_add_custom_slide"),
|
|
189
|
+
element_type: z.enum(["text", "shape", "chart", "table"]).describe("Type of element to add"),
|
|
190
|
+
props: z.record(z.any()).describe(
|
|
191
|
+
`Element properties (all positions in inches, slide is 13.33x7.50):
|
|
192
|
+
|
|
193
|
+
TEXT: {x, y, w, h, text (string or [{text,options:{bold,italic,fontSize,color,fontFace}}]), fontSize, color, bold, italic, fontFace, align, valign, fill, bullet:{type:'bullet',color}, paraSpaceAfter, rectRadius, lineSpacing}
|
|
194
|
+
|
|
195
|
+
SHAPE: {x, y, w, h, shape ('rect','ellipse','roundRect','line'), fill, line:{color,width}, rectRadius, rotate}
|
|
196
|
+
|
|
197
|
+
CHART: {x, y, w, h, chartType ('bar','line','pie','doughnut'), data:[{name,labels:[],values:[]}], chartColors:[], showValue, showLegend, legendPos}
|
|
198
|
+
|
|
199
|
+
TABLE: {x, y, w, h, rows (2D array of cell values), colW (array of column widths), rowH, fontSize, color, headerFill, headerColor, border:{type,color,pt}, autoPage}`
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
async ({ presentation_id, slide_index, element_type, props }) => {
|
|
203
|
+
const pres = presentations.get(presentation_id);
|
|
204
|
+
if (!pres) return { content: [{ type: "text", text: "Error: Presentation not found." }], isError: true };
|
|
205
|
+
const slide = pres.slides && pres.slides[slide_index];
|
|
206
|
+
if (!slide) return { content: [{ type: "text", text: `Error: Slide ${slide_index} not found. Use pptx_add_custom_slide first.` }], isError: true };
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
switch (element_type) {
|
|
210
|
+
case "text": {
|
|
211
|
+
const textContent = props.text || "";
|
|
212
|
+
const textOpts = {
|
|
213
|
+
x: props.x || 0, y: props.y || 0,
|
|
214
|
+
w: props.w || 4, h: props.h || 1,
|
|
215
|
+
fontSize: props.fontSize || 14,
|
|
216
|
+
color: props.color || "333333",
|
|
217
|
+
fontFace: props.fontFace || BRAND.font,
|
|
218
|
+
bold: props.bold || false,
|
|
219
|
+
italic: props.italic || false,
|
|
220
|
+
align: props.align || undefined,
|
|
221
|
+
valign: props.valign || undefined,
|
|
222
|
+
paraSpaceAfter: props.paraSpaceAfter || undefined,
|
|
223
|
+
lineSpacing: props.lineSpacing || undefined,
|
|
224
|
+
};
|
|
225
|
+
if (props.fill) textOpts.fill = { color: props.fill };
|
|
226
|
+
if (props.rectRadius) textOpts.rectRadius = props.rectRadius;
|
|
227
|
+
if (props.bullet) textOpts.bullet = props.bullet;
|
|
228
|
+
if (props.shape) textOpts.shape = props.shape;
|
|
229
|
+
|
|
230
|
+
// Support rich text array: [{text, options}]
|
|
231
|
+
if (Array.isArray(textContent)) {
|
|
232
|
+
slide.addText(textContent, textOpts);
|
|
233
|
+
} else {
|
|
234
|
+
slide.addText(textContent, textOpts);
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case "shape": {
|
|
240
|
+
const shapeMap = {
|
|
241
|
+
rect: "rect", ellipse: "ellipse", roundRect: "roundRect",
|
|
242
|
+
line: "line", triangle: "triangle", diamond: "diamond",
|
|
243
|
+
};
|
|
244
|
+
const pptx = pres.pptx;
|
|
245
|
+
const shapeType = pptx.ShapeType[props.shape || "rect"];
|
|
246
|
+
const shapeOpts = {
|
|
247
|
+
x: props.x || 0, y: props.y || 0,
|
|
248
|
+
w: props.w || 2, h: props.h || 2,
|
|
249
|
+
};
|
|
250
|
+
if (props.fill) shapeOpts.fill = { color: props.fill };
|
|
251
|
+
if (props.line) shapeOpts.line = props.line;
|
|
252
|
+
if (props.rectRadius) shapeOpts.rectRadius = props.rectRadius;
|
|
253
|
+
if (props.rotate) shapeOpts.rotate = props.rotate;
|
|
254
|
+
slide.addShape(shapeType, shapeOpts);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case "chart": {
|
|
259
|
+
const pptx = pres.pptx;
|
|
260
|
+
const chartTypeMap = {
|
|
261
|
+
bar: pptx.ChartType.bar,
|
|
262
|
+
line: pptx.ChartType.line,
|
|
263
|
+
pie: pptx.ChartType.pie,
|
|
264
|
+
doughnut: pptx.ChartType.doughnut,
|
|
265
|
+
area: pptx.ChartType.area,
|
|
266
|
+
};
|
|
267
|
+
const chartType = chartTypeMap[props.chartType || "bar"];
|
|
268
|
+
const chartOpts = {
|
|
269
|
+
x: props.x || 0.5, y: props.y || 1.5,
|
|
270
|
+
w: props.w || 6, h: props.h || 4,
|
|
271
|
+
showValue: props.showValue !== undefined ? props.showValue : true,
|
|
272
|
+
showLegend: props.showLegend || false,
|
|
273
|
+
legendPos: props.legendPos || "b",
|
|
274
|
+
};
|
|
275
|
+
if (props.chartColors) chartOpts.chartColors = props.chartColors;
|
|
276
|
+
slide.addChart(chartType, props.data || [], chartOpts);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case "table": {
|
|
281
|
+
const tableOpts = {
|
|
282
|
+
x: props.x || 0.5, y: props.y || 1.5,
|
|
283
|
+
w: props.w || undefined,
|
|
284
|
+
h: props.h || undefined,
|
|
285
|
+
fontSize: props.fontSize || 12,
|
|
286
|
+
color: props.color || "333333",
|
|
287
|
+
fontFace: BRAND.font,
|
|
288
|
+
autoPage: props.autoPage || false,
|
|
289
|
+
};
|
|
290
|
+
if (props.colW) tableOpts.colW = props.colW;
|
|
291
|
+
if (props.rowH) tableOpts.rowH = props.rowH;
|
|
292
|
+
if (props.border) tableOpts.border = props.border;
|
|
293
|
+
|
|
294
|
+
const rows = (props.rows || []).map((row, rowIdx) =>
|
|
295
|
+
row.map(cell => {
|
|
296
|
+
const isHeader = rowIdx === 0 && props.headerFill;
|
|
297
|
+
return {
|
|
298
|
+
text: String(cell),
|
|
299
|
+
options: {
|
|
300
|
+
fill: isHeader ? { color: props.headerFill } : undefined,
|
|
301
|
+
color: isHeader ? (props.headerColor || "FFFFFF") : props.color || "333333",
|
|
302
|
+
bold: isHeader ? true : false,
|
|
303
|
+
fontSize: props.fontSize || 12,
|
|
304
|
+
fontFace: BRAND.font,
|
|
305
|
+
valign: "middle",
|
|
306
|
+
margin: [4, 6, 4, 6],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
slide.addTable(rows, tableOpts);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: "text", text: JSON.stringify({ message: `${element_type} element added to slide ${slide_index}` }) }],
|
|
318
|
+
};
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return { content: [{ type: "text", text: `Error adding element: ${err.message}` }], isError: true };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// ─── Tool: export ──────────────────────────────────────────────────────
|
|
326
|
+
server.tool(
|
|
327
|
+
"pptx_export",
|
|
328
|
+
"Export the presentation as a .pptx file",
|
|
329
|
+
{
|
|
330
|
+
presentation_id: z.string().describe("The presentation ID"),
|
|
331
|
+
output_path: z.string().optional().describe("Output file path. Defaults to ./output/<title>.pptx"),
|
|
332
|
+
},
|
|
333
|
+
async ({ presentation_id, output_path }) => {
|
|
334
|
+
const pres = presentations.get(presentation_id);
|
|
335
|
+
if (!pres) return { content: [{ type: "text", text: "Error: Presentation not found." }], isError: true };
|
|
336
|
+
const sanitized = (pres.title || "presentation").replace(/[^a-zA-Z0-9_-]/g, "_").substring(0, 50);
|
|
337
|
+
const defaultDir = path.join(process.cwd(), "output");
|
|
338
|
+
if (!fs.existsSync(defaultDir)) fs.mkdirSync(defaultDir, { recursive: true });
|
|
339
|
+
const filePath = output_path || path.join(defaultDir, `${sanitized}.pptx`);
|
|
340
|
+
const dir = path.dirname(filePath);
|
|
341
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
342
|
+
await pres.pptx.writeFile({ fileName: filePath });
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: "text", text: JSON.stringify({ message: "Presentation exported", file_path: filePath, total_slides: pres.slideCount }) }],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// ─── Tool: delete ──────────────────────────────────────────────────────
|
|
350
|
+
server.tool(
|
|
351
|
+
"pptx_delete",
|
|
352
|
+
"Delete a presentation from memory",
|
|
353
|
+
{ presentation_id: z.string().describe("The presentation ID") },
|
|
354
|
+
async ({ presentation_id }) => {
|
|
355
|
+
if (presentations.delete(presentation_id)) {
|
|
356
|
+
return { content: [{ type: "text", text: "Presentation deleted." }] };
|
|
357
|
+
}
|
|
358
|
+
return { content: [{ type: "text", text: "Not found." }], isError: true };
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
module.exports = { register };
|