@tday-com/mcp 0.3.1 → 0.4.2
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/README.md +79 -67
- package/dist/cli.js +781 -63
- package/package.json +19 -11
- package/dist/api-client.js +0 -170
- package/dist/config.js +0 -62
- package/dist/download-safety.js +0 -73
- package/dist/installer-agents.js +0 -199
- package/dist/installer-callback.js +0 -84
- package/dist/installer-slash-command.js +0 -321
- package/dist/installer.js +0 -150
- package/dist/package-info.js +0 -23
- package/dist/prompts.js +0 -36
- package/dist/result-format.js +0 -130
- package/dist/server.js +0 -43
- package/dist/tool-schemas.js +0 -175
- package/dist/tools-account.js +0 -30
- package/dist/tools-features.js +0 -39
- package/dist/tools-launch.js +0 -358
- package/dist/tools.js +0 -275
package/dist/tools.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { open } from "node:fs/promises";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { api, firstImageUrl, normalizeWebsite, query, } from "./api-client.js";
|
|
5
|
-
import { MAX_IMAGE_BYTES, prepareOutputFilePath, validateImageUrl, } from "./download-safety.js";
|
|
6
|
-
import { argsObject, emptyArgsSchema, listArgsSchema, outputSchemas, readOnly, safeIdSchema, safePath, stripPresentationArgs, writeLocal, writeRemote, } from "./tool-schemas.js";
|
|
7
|
-
import { accountTools, imageFormatSchema } from "./tools-account.js";
|
|
8
|
-
import { launchTools } from "./tools-launch.js";
|
|
9
|
-
import { featureTools } from "./tools-features.js";
|
|
10
|
-
const PLATFORM_BADGE_NOTE = 'This image carries a "Made with tday.com" badge because the organisation is on the Free plan. The owner can remove it by upgrading (tday_whoami has the link).';
|
|
11
|
-
export { responseFormatFromArgs, safePath } from "./tool-schemas.js";
|
|
12
|
-
export { ALLOWED_IMAGE_HOSTS, MAX_IMAGE_BYTES, prepareOutputFilePath, sandboxFilePath, validateImageUrl, } from "./download-safety.js";
|
|
13
|
-
export { formatToolResultText, stringifyResult, toStructuredContent, } from "./result-format.js";
|
|
14
|
-
export const tools = [
|
|
15
|
-
{
|
|
16
|
-
name: "tday_whoami",
|
|
17
|
-
title: "Who am I?",
|
|
18
|
-
description: "Get the linked tday account: every organisation the person belongs to, their role in it (owner, admin or member), its plan, its credit balance and when it refreshes, what the plan's output includes (sizes, the Free badge, voiceover), what upgrading would change, its brands, and links to its usage and billing pages. Each organisation has a `summary` sentence to show the person as it is. A design or launch video spends the credits of its BRAND's organisation, so check the organisation that owns the brand before quoting a balance. Only an owner can change a plan; for an admin or member, say who the owner is instead of telling them to upgrade. Use first to verify MCP is connected to the expected account.",
|
|
19
|
-
inputSchema: emptyArgsSchema,
|
|
20
|
-
outputSchema: outputSchemas.whoami,
|
|
21
|
-
annotations: readOnly,
|
|
22
|
-
handler: (_args, config) => api(config, "/api/v1/account"),
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
name: "tday_list_brands",
|
|
26
|
-
title: "List brands",
|
|
27
|
-
description: "List brands accessible to the authenticated user as lightweight summaries. Supports API-backed limit/offset pagination.",
|
|
28
|
-
inputSchema: listArgsSchema,
|
|
29
|
-
outputSchema: outputSchemas.listBrands,
|
|
30
|
-
annotations: readOnly,
|
|
31
|
-
handler: (args, config) => api(config, `/api/brands/summary${query({ limit: Number(args.limit), offset: Number(args.offset) })}`),
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "tday_get_brand",
|
|
35
|
-
title: "Get brand",
|
|
36
|
-
description: "Fetch a single brand with its colors, logos, fonts, images, and knowledge base. Use after tday_list_brands when detailed brand context is needed.",
|
|
37
|
-
inputSchema: argsObject({
|
|
38
|
-
brandId: safeIdSchema.describe("Brand ID from tday"),
|
|
39
|
-
}),
|
|
40
|
-
outputSchema: outputSchemas.getBrand,
|
|
41
|
-
annotations: readOnly,
|
|
42
|
-
handler: (args, config) => api(config, safePath("/api/brands/:brandId", args)),
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: "tday_list_designs",
|
|
46
|
-
title: "List designs",
|
|
47
|
-
description: "List the authenticated user's designs as lightweight summaries. Supports API-backed limit/offset pagination.",
|
|
48
|
-
inputSchema: listArgsSchema,
|
|
49
|
-
outputSchema: outputSchemas.listDesigns,
|
|
50
|
-
annotations: readOnly,
|
|
51
|
-
handler: (args, config) => api(config, `/api/designs/summary${query({ limit: Number(args.limit), offset: Number(args.offset) })}`),
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: "tday_get_design",
|
|
55
|
-
title: "Get design",
|
|
56
|
-
description: "Fetch a single design by id, including its generations. Use after tday_list_designs when generation IDs or full design details are needed.",
|
|
57
|
-
inputSchema: argsObject({
|
|
58
|
-
designId: safeIdSchema.describe("Design ID from tday"),
|
|
59
|
-
}),
|
|
60
|
-
outputSchema: outputSchemas.getDesign,
|
|
61
|
-
annotations: readOnly,
|
|
62
|
-
handler: (args, config) => api(config, safePath("/api/designs/:designId", args)),
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "tday_generate_design",
|
|
66
|
-
title: "Generate design",
|
|
67
|
-
description: "Generate a new tday design from a text prompt against a brand, then poll with tday_poll_generation until it is completed or failed. An image costs 10 credits from the brand's organisation (tday_get_credit_prices has every price; tday_whoami has the balance) and a failed generation gives them back. The response says which organisation paid and what it has left; a refusal for credits means nothing was started, so do not retry. Omit format for square (1080x1080), or landscape (1200x630) where the plan does not include square.",
|
|
68
|
-
inputSchema: argsObject({
|
|
69
|
-
prompt: z
|
|
70
|
-
.string()
|
|
71
|
-
.trim()
|
|
72
|
-
.min(1)
|
|
73
|
-
.max(5000)
|
|
74
|
-
.describe("Design prompt. Be specific about audience, format, copy, and visual direction."),
|
|
75
|
-
brandId: safeIdSchema.describe("Brand ID from tday"),
|
|
76
|
-
name: z.string().trim().min(1).max(200).optional().describe("Short design name"),
|
|
77
|
-
format: imageFormatSchema,
|
|
78
|
-
}),
|
|
79
|
-
outputSchema: outputSchemas.generate,
|
|
80
|
-
annotations: writeRemote,
|
|
81
|
-
handler: (args, config) => api(config, "/api/v1/generate", {
|
|
82
|
-
method: "POST",
|
|
83
|
-
body: JSON.stringify(stripPresentationArgs(args)),
|
|
84
|
-
}),
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "tday_poll_generation",
|
|
88
|
-
title: "Poll generation",
|
|
89
|
-
description: "Poll a design generation. Returns status (pending/ideating/generating/visualising/reviewing/refining/completed/failed), editorUrl, imageUrl when completed, and platformBadge when the image carries the Free plan's badge. Call repeatedly until status is completed or failed.",
|
|
90
|
-
inputSchema: argsObject({
|
|
91
|
-
designId: safeIdSchema.describe("Design ID from tday"),
|
|
92
|
-
generationId: safeIdSchema.describe("Generation ID from tday"),
|
|
93
|
-
}),
|
|
94
|
-
outputSchema: outputSchemas.poll,
|
|
95
|
-
annotations: { ...readOnly, idempotentHint: true },
|
|
96
|
-
handler: async (args, config) => {
|
|
97
|
-
const path = safePath("/api/designs/:designId/generate/:generationId/status?poll=1", args);
|
|
98
|
-
const data = await api(config, path);
|
|
99
|
-
const generation = data.complete?.generation;
|
|
100
|
-
const status = data.status ?? generation?.status ?? "unknown";
|
|
101
|
-
const imageUrl = firstImageUrl(generation);
|
|
102
|
-
return {
|
|
103
|
-
designId: args.designId,
|
|
104
|
-
generationId: args.generationId,
|
|
105
|
-
status,
|
|
106
|
-
editorUrl: `${config.webBaseUrl}/designs/${args.designId}`,
|
|
107
|
-
...(imageUrl ? { imageUrl } : {}),
|
|
108
|
-
...(generation?.completedAt ? { completedAt: generation.completedAt } : {}),
|
|
109
|
-
...(data.platformBadge ? { platformBadge: true, badgeNote: PLATFORM_BADGE_NOTE } : {}),
|
|
110
|
-
...(status === "failed"
|
|
111
|
-
? { failureMessage: data.complete?.message ?? "Generation did not complete." }
|
|
112
|
-
: {}),
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "tday_create_brand",
|
|
118
|
-
title: "Create brand",
|
|
119
|
-
description: "Create a new brand from a website URL. Extracts colors, fonts, and logos automatically from the site. You can also pass a name and description.",
|
|
120
|
-
inputSchema: argsObject({
|
|
121
|
-
website: z
|
|
122
|
-
.string()
|
|
123
|
-
.trim()
|
|
124
|
-
.min(1)
|
|
125
|
-
.max(2048)
|
|
126
|
-
.describe("Website URL to extract brand assets from, for example stripe.com or https://stripe.com"),
|
|
127
|
-
name: z.string().trim().min(1).max(200).optional().describe("Brand name, auto-detected from website if omitted"),
|
|
128
|
-
description: z.string().trim().min(1).max(1000).optional().describe("Short brand description"),
|
|
129
|
-
industry: z.string().trim().min(1).max(120).optional().describe("Industry category"),
|
|
130
|
-
}),
|
|
131
|
-
outputSchema: outputSchemas.createBrand,
|
|
132
|
-
annotations: writeRemote,
|
|
133
|
-
handler: (args, config) => {
|
|
134
|
-
const body = { website: normalizeWebsite(String(args.website)) };
|
|
135
|
-
if (args.name)
|
|
136
|
-
body.name = args.name;
|
|
137
|
-
if (args.description)
|
|
138
|
-
body.description = args.description;
|
|
139
|
-
if (args.industry)
|
|
140
|
-
body.industry = args.industry;
|
|
141
|
-
return api(config, "/api/brands", {
|
|
142
|
-
method: "POST",
|
|
143
|
-
body: JSON.stringify(body),
|
|
144
|
-
});
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: "tday_download_image",
|
|
149
|
-
title: "Download image",
|
|
150
|
-
description: "Download a completed generated design image to a local file. Pass designId + generationId to save the exported file (what a download from tday gives you), or an imageUrl to save that exact image. Writes only inside the current working directory and caps downloads at 50MB.",
|
|
151
|
-
inputSchema: argsObject({
|
|
152
|
-
imageUrl: z
|
|
153
|
-
.string()
|
|
154
|
-
.trim()
|
|
155
|
-
.url()
|
|
156
|
-
.optional()
|
|
157
|
-
.describe("Completed generation image URL. Optional if designId and generationId are provided."),
|
|
158
|
-
designId: safeIdSchema.optional().describe("Design ID from tday. Optional if imageUrl is provided."),
|
|
159
|
-
generationId: safeIdSchema.optional().describe("Generation ID from tday. Optional if imageUrl is provided."),
|
|
160
|
-
filePath: z
|
|
161
|
-
.string()
|
|
162
|
-
.trim()
|
|
163
|
-
.min(1)
|
|
164
|
-
.max(500)
|
|
165
|
-
.describe("Local path to save the image, relative to cwd, for example ./tday-designs/poster.png"),
|
|
166
|
-
overwrite: z
|
|
167
|
-
.boolean()
|
|
168
|
-
.default(false)
|
|
169
|
-
.describe("Whether to overwrite an existing regular file. Defaults to false for safety."),
|
|
170
|
-
}),
|
|
171
|
-
outputSchema: outputSchemas.download,
|
|
172
|
-
annotations: writeLocal,
|
|
173
|
-
handler: async (args, config) => {
|
|
174
|
-
const hasUrl = Boolean(args.imageUrl);
|
|
175
|
-
const hasIds = Boolean(args.designId && args.generationId);
|
|
176
|
-
if (!hasUrl && !hasIds) {
|
|
177
|
-
throw new Error("Provide either imageUrl, or both designId and generationId.");
|
|
178
|
-
}
|
|
179
|
-
if ((args.designId && !args.generationId) || (!args.designId && args.generationId)) {
|
|
180
|
-
throw new Error("designId and generationId must be provided together.");
|
|
181
|
-
}
|
|
182
|
-
const imageUrl = hasIds
|
|
183
|
-
? await imageUrlForGeneration(args, config)
|
|
184
|
-
: String(args.imageUrl);
|
|
185
|
-
const url = validateImageUrl(imageUrl);
|
|
186
|
-
const filePath = await prepareOutputFilePath(String(args.filePath), {
|
|
187
|
-
overwrite: Boolean(args.overwrite),
|
|
188
|
-
});
|
|
189
|
-
const res = await fetch(url.href);
|
|
190
|
-
if (res.url)
|
|
191
|
-
validateImageUrl(res.url);
|
|
192
|
-
if (!res.ok) {
|
|
193
|
-
throw new Error(`Failed to download image: ${res.status} ${res.statusText}`);
|
|
194
|
-
}
|
|
195
|
-
const contentLength = Number(res.headers.get("content-length") ?? 0);
|
|
196
|
-
if (Number.isFinite(contentLength) && contentLength > MAX_IMAGE_BYTES) {
|
|
197
|
-
throw new Error(`Image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit according to content-length. Download aborted.`);
|
|
198
|
-
}
|
|
199
|
-
const contentType = res.headers.get("content-type");
|
|
200
|
-
if (contentType &&
|
|
201
|
-
!contentType.toLowerCase().startsWith("image/") &&
|
|
202
|
-
!contentType.toLowerCase().startsWith("application/octet-stream")) {
|
|
203
|
-
throw new Error(`Download did not return an image (content-type: ${contentType})`);
|
|
204
|
-
}
|
|
205
|
-
const chunks = [];
|
|
206
|
-
let totalBytes = 0;
|
|
207
|
-
const reader = res.body?.getReader();
|
|
208
|
-
if (!reader) {
|
|
209
|
-
throw new Error("No response body from image URL");
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
for (;;) {
|
|
213
|
-
const { done, value } = await reader.read();
|
|
214
|
-
if (done)
|
|
215
|
-
break;
|
|
216
|
-
totalBytes += value.byteLength;
|
|
217
|
-
if (totalBytes > MAX_IMAGE_BYTES) {
|
|
218
|
-
throw new Error(`Image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit. Download aborted.`);
|
|
219
|
-
}
|
|
220
|
-
chunks.push(value);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
finally {
|
|
224
|
-
reader.releaseLock();
|
|
225
|
-
}
|
|
226
|
-
const buffer = Buffer.concat(chunks);
|
|
227
|
-
const flags = constants.O_WRONLY |
|
|
228
|
-
constants.O_CREAT |
|
|
229
|
-
(args.overwrite ? constants.O_TRUNC : constants.O_EXCL) |
|
|
230
|
-
(constants.O_NOFOLLOW ?? 0);
|
|
231
|
-
const handle = await open(filePath, flags, 0o600);
|
|
232
|
-
try {
|
|
233
|
-
await handle.writeFile(buffer);
|
|
234
|
-
}
|
|
235
|
-
finally {
|
|
236
|
-
await handle.close();
|
|
237
|
-
}
|
|
238
|
-
return {
|
|
239
|
-
saved: true,
|
|
240
|
-
path: filePath,
|
|
241
|
-
size: buffer.byteLength,
|
|
242
|
-
};
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
...accountTools,
|
|
246
|
-
...launchTools,
|
|
247
|
-
...featureTools,
|
|
248
|
-
];
|
|
249
|
-
async function imageUrlForGeneration(args, config) {
|
|
250
|
-
const data = await api(config, safePath("/api/designs/:designId/generations/:generationId/export", args), { method: "POST", body: JSON.stringify({ scale: 1 }) });
|
|
251
|
-
const imageUrl = typeof data?.image?.url === "string" ? data.image.url : null;
|
|
252
|
-
if (!imageUrl) {
|
|
253
|
-
throw new Error("No image URL found for this generation. Poll tday_poll_generation until status is completed, then try again.");
|
|
254
|
-
}
|
|
255
|
-
return imageUrl;
|
|
256
|
-
}
|
|
257
|
-
function formatZodError(name, error) {
|
|
258
|
-
const details = error.issues
|
|
259
|
-
.map((issue) => {
|
|
260
|
-
const path = issue.path.length ? issue.path.join(".") : "arguments";
|
|
261
|
-
return `${path}: ${issue.message}`;
|
|
262
|
-
})
|
|
263
|
-
.join("; ");
|
|
264
|
-
return `Invalid arguments for ${name}: ${details}`;
|
|
265
|
-
}
|
|
266
|
-
export async function callTool(name, args, config, available = tools) {
|
|
267
|
-
const tool = available.find((t) => t.name === name);
|
|
268
|
-
if (!tool) {
|
|
269
|
-
throw new Error(`Unknown tool: ${name}. Available tools: ${available.map((t) => t.name).join(", ")}`);
|
|
270
|
-
}
|
|
271
|
-
const parsed = tool.inputSchema.safeParse(args);
|
|
272
|
-
if (!parsed.success)
|
|
273
|
-
throw new Error(formatZodError(tool.name, parsed.error));
|
|
274
|
-
return tool.handler(parsed.data, config);
|
|
275
|
-
}
|