@tday-com/mcp 0.0.0-stage → 0.3.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 +36 -0
- package/README.md +124 -2
- package/dist/api-client.js +170 -0
- package/dist/cli.js +75 -0
- package/dist/config.js +62 -0
- package/dist/download-safety.js +73 -0
- package/dist/installer-agents.js +199 -0
- package/dist/installer-callback.js +84 -0
- package/dist/installer-slash-command.js +321 -0
- package/dist/installer.js +150 -0
- package/dist/package-info.js +23 -0
- package/dist/prompts.js +36 -0
- package/dist/result-format.js +130 -0
- package/dist/server.js +43 -0
- package/dist/tool-schemas.js +175 -0
- package/dist/tools-account.js +30 -0
- package/dist/tools-features.js +39 -0
- package/dist/tools-launch.js +358 -0
- package/dist/tools.js +275 -0
- package/package.json +48 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { startCallbackServer } from "./installer-callback.js";
|
|
7
|
+
import { patchAllAgents, previewAllAgents } from "./installer-agents.js";
|
|
8
|
+
import { installSlashCommands } from "./installer-slash-command.js";
|
|
9
|
+
import { capturePublished } from "./package-info.js";
|
|
10
|
+
const WEB_URL = (process.env.TDAY_WEB_URL ?? "https://tday.com").replace(/\/+$/, "");
|
|
11
|
+
const API_URL = (process.env.TDAY_API_URL ?? WEB_URL).replace(/\/+$/, "").replace(/\/api$/i, "");
|
|
12
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
|
+
export async function runInstaller(args) {
|
|
14
|
+
const skipBrowser = args.includes("--no-browser");
|
|
15
|
+
const dryRun = args.includes("--dry-run");
|
|
16
|
+
console.log("tday MCP installer\n");
|
|
17
|
+
if (dryRun) {
|
|
18
|
+
console.log("Dry run - no files will be written, no browser opened.\n");
|
|
19
|
+
const results = previewAllAgents({ capture: true });
|
|
20
|
+
for (const r of results) {
|
|
21
|
+
console.log(` [${r.action}] ${r.name} - ${r.message}`);
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const state = randomBytes(16).toString("hex");
|
|
26
|
+
const clientName = detectClientLabel();
|
|
27
|
+
console.log("1. Starting local callback listener…");
|
|
28
|
+
const { port, done } = await startCallbackServer({
|
|
29
|
+
expectedState: state,
|
|
30
|
+
timeoutMs: TIMEOUT_MS,
|
|
31
|
+
});
|
|
32
|
+
const callbackUrl = `http://127.0.0.1:${port}/cb`;
|
|
33
|
+
const authUrl = `${WEB_URL}/cli/auth` +
|
|
34
|
+
`?callback=${encodeURIComponent(callbackUrl)}` +
|
|
35
|
+
`&state=${encodeURIComponent(state)}` +
|
|
36
|
+
`&client=${encodeURIComponent(clientName)}`;
|
|
37
|
+
console.log("2. Sign in to tday in your browser to authorize this MCP connection:\n");
|
|
38
|
+
console.log(` ${authUrl}\n`);
|
|
39
|
+
if (!skipBrowser) {
|
|
40
|
+
openBrowser(authUrl);
|
|
41
|
+
}
|
|
42
|
+
console.log("3. Waiting for authorization (timeout 5 min)…\n");
|
|
43
|
+
let token;
|
|
44
|
+
try {
|
|
45
|
+
const result = await done;
|
|
46
|
+
token = result.token;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error(`\nAuthorization failed: ${err.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
console.log("4. Verifying account link…");
|
|
53
|
+
const ok = await verifyToken(token);
|
|
54
|
+
if (!ok) {
|
|
55
|
+
console.error(`Account link verification failed against ${API_URL}/api/billing/info. Nothing was saved.`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
saveConfig(token);
|
|
59
|
+
console.log(` Saved secure local connection to ${configPath()} (chmod 0600)\n`);
|
|
60
|
+
const access = await accountAccess(token);
|
|
61
|
+
console.log("5. Patching installed AI agents (Claude Code, Cursor, Gemini CLI, Codex, VS Code)…");
|
|
62
|
+
const results = patchAllAgents({ capture: access.capture });
|
|
63
|
+
for (const r of results) {
|
|
64
|
+
const tag = r.action === "patched" ? "+" : r.action === "manual" ? "!" : "-";
|
|
65
|
+
console.log(` ${tag} ${r.name}: ${r.message}`);
|
|
66
|
+
}
|
|
67
|
+
console.log("\n6. Installing Claude Code slash commands...");
|
|
68
|
+
const cmds = installSlashCommands(access);
|
|
69
|
+
for (const cmd of cmds.filter((c) => c.installed))
|
|
70
|
+
console.log(` + ${cmd.path}`);
|
|
71
|
+
if (cmds.some((cmd) => !cmd.installed))
|
|
72
|
+
console.log(" - Skipped (Claude Code not found)");
|
|
73
|
+
const names = cmds.filter((cmd) => cmd.installed).map((cmd) => `/${cmd.name}`);
|
|
74
|
+
console.log(`\nDone. Restart your AI agent.${names.length ? ` In Claude Code, type / to see: ${names.join(", ")}` : ""}`);
|
|
75
|
+
}
|
|
76
|
+
async function accountAccess(token) {
|
|
77
|
+
const read = async (path) => {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(`${API_URL}${path}`, { headers: { authorization: `Bearer ${token}`, accept: "application/json" } });
|
|
80
|
+
if (!res.ok)
|
|
81
|
+
return null;
|
|
82
|
+
const body = (await res.json());
|
|
83
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const [me, account, published] = await Promise.all([read("/api/capture/me"), read("/api/v1/account"), capturePublished()]);
|
|
90
|
+
const brands = published && Array.isArray(me?.brands) ? me.brands : [];
|
|
91
|
+
const features = account?.features;
|
|
92
|
+
return {
|
|
93
|
+
capture: brands.length > 0,
|
|
94
|
+
demos: brands.some((brand) => brand?.localDemos === true),
|
|
95
|
+
launchVideos: features?.launchVideos === true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function configPath() {
|
|
99
|
+
return join(homedir(), ".tday", "config.json");
|
|
100
|
+
}
|
|
101
|
+
function saveConfig(token) {
|
|
102
|
+
const path = configPath();
|
|
103
|
+
const dir = dirname(path);
|
|
104
|
+
if (!existsSync(dir))
|
|
105
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
106
|
+
chmodSync(dir, 0o700);
|
|
107
|
+
writeFileSync(path, JSON.stringify({ token, apiBaseUrl: API_URL, webBaseUrl: WEB_URL }, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
108
|
+
chmodSync(path, 0o600);
|
|
109
|
+
}
|
|
110
|
+
async function verifyToken(token) {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(`${API_URL}/api/billing/info`, {
|
|
113
|
+
headers: { authorization: `Bearer ${token}` },
|
|
114
|
+
});
|
|
115
|
+
return res.ok;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function detectClientLabel() {
|
|
122
|
+
if (process.env.TDAY_CLIENT_LABEL)
|
|
123
|
+
return process.env.TDAY_CLIENT_LABEL;
|
|
124
|
+
if (process.env.TERM_PROGRAM === "Claude Code")
|
|
125
|
+
return "claude-code";
|
|
126
|
+
if (process.env.CURSOR_TRACE_ID)
|
|
127
|
+
return "cursor";
|
|
128
|
+
return "tday-cli";
|
|
129
|
+
}
|
|
130
|
+
export function openBrowser(url) {
|
|
131
|
+
const platform = process.platform;
|
|
132
|
+
const warn = () => console.warn("Could not open the browser. Open the printed URL manually to continue.");
|
|
133
|
+
try {
|
|
134
|
+
let child;
|
|
135
|
+
if (platform === "darwin") {
|
|
136
|
+
child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
137
|
+
}
|
|
138
|
+
else if (platform === "win32") {
|
|
139
|
+
child = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
143
|
+
}
|
|
144
|
+
child.once("error", warn);
|
|
145
|
+
child.unref();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
warn();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export const PACKAGE_NAME = "@tday-com/mcp";
|
|
3
|
+
export function packageVersion() {
|
|
4
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5
|
+
return pkg.version;
|
|
6
|
+
}
|
|
7
|
+
export function pinnedPackageSpec() {
|
|
8
|
+
return `${PACKAGE_NAME}@${packageVersion()}`;
|
|
9
|
+
}
|
|
10
|
+
export const CAPTURE_PACKAGE_NAME = "@tday-com/capture";
|
|
11
|
+
export const CAPTURE_PACKAGE_VERSION = "0.2.0";
|
|
12
|
+
export function pinnedCaptureSpec() {
|
|
13
|
+
return `${CAPTURE_PACKAGE_NAME}@${CAPTURE_PACKAGE_VERSION}`;
|
|
14
|
+
}
|
|
15
|
+
export async function capturePublished() {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`https://registry.npmjs.org/${CAPTURE_PACKAGE_NAME.replace("/", "%2f")}/${CAPTURE_PACKAGE_VERSION}`);
|
|
18
|
+
return res.ok;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function featureArg(raw) {
|
|
3
|
+
const text = (raw ?? "").replace(/["\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
4
|
+
return text || null;
|
|
5
|
+
}
|
|
6
|
+
export function launchVideoPrompt(raw) {
|
|
7
|
+
const feature = featureArg(raw);
|
|
8
|
+
return [
|
|
9
|
+
`Make a tday launch video of ${feature ? `the feature "${feature}"` : "the newest feature that has no launch video yet"}.`,
|
|
10
|
+
feature
|
|
11
|
+
? `1. Find it with tday_list_features (search "${feature}") and use its id. If several match, show them and ask which.`
|
|
12
|
+
: '1. Call tday_list_features with without "launch_video" and offer the newest feature it lists, with the next two as alternatives.',
|
|
13
|
+
" If tday asks for a brand, list them with tday_list_brands and ask which.",
|
|
14
|
+
"2. Before spending anything, quote the price (tday_get_credit_prices) and the balance (tday_whoami), and ask me to confirm and whether I want a voiceover or another length.",
|
|
15
|
+
"3. Call tday_create_launch_video with the featureId and the options I chose, then poll tday_poll_launch_video every 20-30 seconds until it is ready.",
|
|
16
|
+
"4. Show me the reviewUrl as a link and offer the MP4 (tday_get_launch_video_mp4).",
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
export const FEATURES_PROMPT = [
|
|
20
|
+
"Show me my tday features, newest first, and which have a launch video: call tday_list_features (if tday asks for a brand, list them with tday_list_brands and ask which).",
|
|
21
|
+
"List each feature's name, when it was added, and its launch video: finished, being made, or none.",
|
|
22
|
+
"Then offer to make a launch video of the newest feature that has none.",
|
|
23
|
+
].join("\n");
|
|
24
|
+
const userText = (text) => ({ messages: [{ role: "user", content: { type: "text", text } }] });
|
|
25
|
+
export function registerPrompts(server) {
|
|
26
|
+
const registerPrompt = server.registerPrompt.bind(server);
|
|
27
|
+
registerPrompt("launch-video", {
|
|
28
|
+
title: "Launch video of a feature",
|
|
29
|
+
description: "Make a launch video of a feature: the one you name, or the newest feature without one. Quotes the credits before it starts.",
|
|
30
|
+
argsSchema: { feature: z.string().optional().describe("The feature's name. Leave empty for the newest feature without a launch video.") },
|
|
31
|
+
}, ({ feature }) => userText(launchVideoPrompt(feature)));
|
|
32
|
+
registerPrompt("features", {
|
|
33
|
+
title: "Latest features",
|
|
34
|
+
description: "The brand's newest features and which already have a launch video.",
|
|
35
|
+
}, () => userText(FEATURES_PROMPT));
|
|
36
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const MAX_TEXT_CHARS = 25_000;
|
|
2
|
+
export function stringifyResult(result) {
|
|
3
|
+
return JSON.stringify(result, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2);
|
|
4
|
+
}
|
|
5
|
+
export function toStructuredContent(result) {
|
|
6
|
+
return isRecord(result) ? result : { result };
|
|
7
|
+
}
|
|
8
|
+
export function formatToolResultText(toolName, result, format, markdown) {
|
|
9
|
+
const text = format === "markdown" ? markdownResult(toolName, result, markdown) : stringifyResult(result);
|
|
10
|
+
return truncateText(text);
|
|
11
|
+
}
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function truncateText(text) {
|
|
16
|
+
if (text.length <= MAX_TEXT_CHARS)
|
|
17
|
+
return text;
|
|
18
|
+
return `${text.slice(0, MAX_TEXT_CHARS)}\n\n[Response text truncated at ${MAX_TEXT_CHARS} characters. Use structuredContent or narrower list/get calls for the complete data.]`;
|
|
19
|
+
}
|
|
20
|
+
function markdownResult(toolName, result, markdown) {
|
|
21
|
+
if (!isRecord(result))
|
|
22
|
+
return String(result);
|
|
23
|
+
if (markdown)
|
|
24
|
+
return markdown(result);
|
|
25
|
+
switch (toolName) {
|
|
26
|
+
case "tday_whoami":
|
|
27
|
+
return markdownWhoami(result);
|
|
28
|
+
case "tday_list_brands":
|
|
29
|
+
return markdownList("Brands", result.brands, ["id", "name", "role", "status", "industry"]);
|
|
30
|
+
case "tday_list_designs":
|
|
31
|
+
return markdownList("Designs", result.designs, ["id", "name", "brandName", "currentStep", "updatedAt"]);
|
|
32
|
+
case "tday_generate_design":
|
|
33
|
+
return [
|
|
34
|
+
"# Generation queued",
|
|
35
|
+
`- Design ID: ${result.designId ?? "unknown"}`,
|
|
36
|
+
`- Generation ID: ${result.generationId ?? "unknown"}`,
|
|
37
|
+
`- Status: ${result.status ?? "unknown"}`,
|
|
38
|
+
result.editorUrl ? `- Editor: ${result.editorUrl}` : undefined,
|
|
39
|
+
"",
|
|
40
|
+
"Poll with `tday_poll_generation` until status is `completed` or `failed`.",
|
|
41
|
+
].filter(Boolean).join("\n");
|
|
42
|
+
case "tday_poll_generation":
|
|
43
|
+
return [
|
|
44
|
+
"# Generation status",
|
|
45
|
+
`- Design ID: ${result.designId ?? "unknown"}`,
|
|
46
|
+
`- Generation ID: ${result.generationId ?? "unknown"}`,
|
|
47
|
+
`- Status: ${result.status ?? "unknown"}`,
|
|
48
|
+
result.imageUrl ? `- Image URL: ${result.imageUrl}` : undefined,
|
|
49
|
+
result.editorUrl ? `- Editor: ${result.editorUrl}` : undefined,
|
|
50
|
+
result.failureMessage ? `- Failure: ${result.failureMessage}` : undefined,
|
|
51
|
+
].filter(Boolean).join("\n");
|
|
52
|
+
case "tday_download_image":
|
|
53
|
+
return [
|
|
54
|
+
"# Image downloaded",
|
|
55
|
+
`- Saved: ${result.saved ? "yes" : "no"}`,
|
|
56
|
+
`- Path: ${result.path ?? "unknown"}`,
|
|
57
|
+
`- Size: ${result.size ?? 0} bytes`,
|
|
58
|
+
].join("\n");
|
|
59
|
+
default:
|
|
60
|
+
return `\`\`\`json\n${stringifyResult(result)}\n\`\`\``;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function markdownWhoami(result) {
|
|
64
|
+
const account = isRecord(result.account) ? result.account : {};
|
|
65
|
+
const subscription = isRecord(result.subscription) ? result.subscription : {};
|
|
66
|
+
return [
|
|
67
|
+
"# tday account",
|
|
68
|
+
`- Name: ${display(account.name)}`,
|
|
69
|
+
`- Email: ${display(account.email)}`,
|
|
70
|
+
`- Auth source: ${display(isRecord(result.auth) ? result.auth.source : undefined)}`,
|
|
71
|
+
`- Plan: ${display(subscription.plan ?? subscription.name ?? subscription.tier)}`,
|
|
72
|
+
balanceLine(result),
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
function balanceLine(result) {
|
|
76
|
+
const credits = isRecord(result.credits) ? result.credits : null;
|
|
77
|
+
if (credits && credits.enabled === true) {
|
|
78
|
+
if (typeof credits.balance !== "number")
|
|
79
|
+
return `- Credits: ${display(credits.balance)}`;
|
|
80
|
+
if (credits.grant === null)
|
|
81
|
+
return "- Credits: usage is covered by your contract";
|
|
82
|
+
const grant = typeof credits.grant === "number" ? ` of ${formatCount(credits.grant)}` : "";
|
|
83
|
+
const refresh = typeof credits.refreshesAt === "string" ? `, refreshes ${formatDay(credits.refreshesAt)}` : "";
|
|
84
|
+
const low = credits.lowBalance === "low" || credits.lowBalance === "critical" ? " (running low)" : "";
|
|
85
|
+
return `- Credits: ${formatCount(credits.balance)}${grant} left${refresh}${low}`;
|
|
86
|
+
}
|
|
87
|
+
const legacy = isRecord(result.apiBalance) ? result.apiBalance : null;
|
|
88
|
+
if (legacy && typeof legacy.balanceCents === "number") {
|
|
89
|
+
return `- Prepaid balance: $${(legacy.balanceCents / 100).toFixed(2)}`;
|
|
90
|
+
}
|
|
91
|
+
return `- Balance: ${display(result.apiBalance)}`;
|
|
92
|
+
}
|
|
93
|
+
function formatCount(value) {
|
|
94
|
+
return Math.trunc(value).toLocaleString("en-US");
|
|
95
|
+
}
|
|
96
|
+
function formatDay(iso) {
|
|
97
|
+
const date = new Date(iso);
|
|
98
|
+
if (Number.isNaN(date.getTime()))
|
|
99
|
+
return iso;
|
|
100
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
101
|
+
}
|
|
102
|
+
export function markdownList(title, value, columns) {
|
|
103
|
+
const items = Array.isArray(value) ? value.filter(isRecord) : [];
|
|
104
|
+
const lines = [`# ${title}`, ""];
|
|
105
|
+
if (items.length === 0) {
|
|
106
|
+
lines.push("No items returned.");
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
lines.push(`Returned ${items.length} item${items.length === 1 ? "" : "s"}.`, "");
|
|
110
|
+
lines.push(`| ${columns.join(" | ")} |`);
|
|
111
|
+
lines.push(`| ${columns.map(() => "---").join(" | ")} |`);
|
|
112
|
+
for (const item of items) {
|
|
113
|
+
lines.push(`| ${columns.map((column) => escapeMarkdownCell(display(item[column]))).join(" | ")} |`);
|
|
114
|
+
}
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
export function display(value) {
|
|
118
|
+
if (value === null || value === undefined || value === "")
|
|
119
|
+
return "-";
|
|
120
|
+
if (typeof value === "string" ||
|
|
121
|
+
typeof value === "number" ||
|
|
122
|
+
typeof value === "boolean" ||
|
|
123
|
+
typeof value === "bigint") {
|
|
124
|
+
return String(value);
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(value, (_key, v) => (typeof v === "bigint" ? v.toString() : v));
|
|
127
|
+
}
|
|
128
|
+
function escapeMarkdownCell(value) {
|
|
129
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
130
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { packageVersion } from "./package-info.js";
|
|
5
|
+
import { registerPrompts } from "./prompts.js";
|
|
6
|
+
import { tools, formatToolResultText, responseFormatFromArgs, toStructuredContent, } from "./tools.js";
|
|
7
|
+
export async function runServer() {
|
|
8
|
+
const config = await loadConfig();
|
|
9
|
+
const server = new McpServer({ name: "tday-mcp-server", version: packageVersion() });
|
|
10
|
+
registerTools(server, tools, config);
|
|
11
|
+
registerPrompts(server);
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
await server.connect(transport);
|
|
14
|
+
}
|
|
15
|
+
export function registerTools(server, list, config) {
|
|
16
|
+
const registerTool = server.registerTool.bind(server);
|
|
17
|
+
for (const tool of list) {
|
|
18
|
+
registerTool(tool.name, {
|
|
19
|
+
title: tool.title,
|
|
20
|
+
description: tool.description,
|
|
21
|
+
inputSchema: tool.inputSchema,
|
|
22
|
+
outputSchema: tool.outputSchema,
|
|
23
|
+
annotations: {
|
|
24
|
+
readOnlyHint: tool.annotations.readOnlyHint,
|
|
25
|
+
destructiveHint: tool.annotations.destructiveHint,
|
|
26
|
+
idempotentHint: tool.annotations.idempotentHint,
|
|
27
|
+
openWorldHint: tool.annotations.openWorldHint,
|
|
28
|
+
},
|
|
29
|
+
}, async (args) => {
|
|
30
|
+
const rawArgs = args ?? {};
|
|
31
|
+
const result = await tool.handler(rawArgs, config);
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: formatToolResultText(tool.name, result, responseFormatFromArgs(rawArgs), tool.markdown),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
structuredContent: toStructuredContent(result),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
3
|
+
const responseFormatSchema = z
|
|
4
|
+
.enum(["json", "markdown"])
|
|
5
|
+
.default("json")
|
|
6
|
+
.describe("Text response format. Defaults to json for backward compatibility; structuredContent is always returned when the MCP client supports it.");
|
|
7
|
+
export const safeIdSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1)
|
|
11
|
+
.regex(SAFE_ID, "must contain only letters, numbers, underscores, and hyphens");
|
|
12
|
+
export function argsObject(shape) {
|
|
13
|
+
return z
|
|
14
|
+
.object({
|
|
15
|
+
...shape,
|
|
16
|
+
response_format: responseFormatSchema,
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
}
|
|
20
|
+
export const emptyArgsSchema = argsObject({});
|
|
21
|
+
export const listArgsSchema = argsObject({
|
|
22
|
+
limit: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(100)
|
|
27
|
+
.default(50)
|
|
28
|
+
.describe("Maximum items to return. Defaults to 50."),
|
|
29
|
+
offset: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(0)
|
|
33
|
+
.default(0)
|
|
34
|
+
.describe("Number of items to skip for pagination. Defaults to 0."),
|
|
35
|
+
});
|
|
36
|
+
export function safePath(template, params) {
|
|
37
|
+
return template.replace(/:(\w+)/g, (_, key) => {
|
|
38
|
+
const val = String(params[key] ?? "");
|
|
39
|
+
if (!SAFE_ID.test(val)) {
|
|
40
|
+
throw new Error(`Invalid ${key}: must contain only letters, numbers, underscores, and hyphens (got "${val}")`);
|
|
41
|
+
}
|
|
42
|
+
return encodeURIComponent(val);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function stripPresentationArgs(args) {
|
|
46
|
+
const { response_format: _responseFormat, ...rest } = args;
|
|
47
|
+
return rest;
|
|
48
|
+
}
|
|
49
|
+
export function responseFormatFromArgs(args) {
|
|
50
|
+
return args.response_format === "markdown" ? "markdown" : "json";
|
|
51
|
+
}
|
|
52
|
+
const anyRecord = z.record(z.unknown());
|
|
53
|
+
const paginationSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
total_count: z.number().optional(),
|
|
56
|
+
total: z.number().optional(),
|
|
57
|
+
count: z.number(),
|
|
58
|
+
offset: z.number(),
|
|
59
|
+
limit: z.number(),
|
|
60
|
+
has_more: z.boolean().optional(),
|
|
61
|
+
hasMore: z.boolean().optional(),
|
|
62
|
+
next_offset: z.number().optional(),
|
|
63
|
+
nextOffset: z.number().optional(),
|
|
64
|
+
})
|
|
65
|
+
.passthrough();
|
|
66
|
+
export const outputSchemas = {
|
|
67
|
+
whoami: z
|
|
68
|
+
.object({
|
|
69
|
+
account: z.unknown().optional(),
|
|
70
|
+
organizations: z.array(anyRecord).optional(),
|
|
71
|
+
defaultOrganizationId: z.string().nullable().optional(),
|
|
72
|
+
features: z.unknown().optional(),
|
|
73
|
+
links: z.unknown().optional(),
|
|
74
|
+
notes: z.array(z.string()).optional(),
|
|
75
|
+
})
|
|
76
|
+
.passthrough(),
|
|
77
|
+
creditPrices: z
|
|
78
|
+
.object({
|
|
79
|
+
priceVersion: z.number().optional(),
|
|
80
|
+
actions: z.array(anyRecord).optional(),
|
|
81
|
+
launchVideos: z.array(anyRecord).optional(),
|
|
82
|
+
launchVideoRevisions: z.array(anyRecord).optional(),
|
|
83
|
+
monthlyCredits: z.unknown().optional(),
|
|
84
|
+
notes: z.array(z.string()).optional(),
|
|
85
|
+
})
|
|
86
|
+
.passthrough(),
|
|
87
|
+
usage: z
|
|
88
|
+
.object({
|
|
89
|
+
organization: anyRecord.optional(),
|
|
90
|
+
balance: z.unknown().optional(),
|
|
91
|
+
usage: z.unknown().optional(),
|
|
92
|
+
links: z.unknown().optional(),
|
|
93
|
+
})
|
|
94
|
+
.passthrough(),
|
|
95
|
+
listBrands: z
|
|
96
|
+
.object({
|
|
97
|
+
brands: z.array(anyRecord),
|
|
98
|
+
pagination: paginationSchema.optional(),
|
|
99
|
+
})
|
|
100
|
+
.passthrough(),
|
|
101
|
+
getBrand: z
|
|
102
|
+
.object({
|
|
103
|
+
brand: anyRecord.optional(),
|
|
104
|
+
role: z.string().optional(),
|
|
105
|
+
})
|
|
106
|
+
.passthrough(),
|
|
107
|
+
listDesigns: z
|
|
108
|
+
.object({
|
|
109
|
+
designs: z.array(anyRecord),
|
|
110
|
+
archivedCount: z.number().optional(),
|
|
111
|
+
pagination: paginationSchema.optional(),
|
|
112
|
+
})
|
|
113
|
+
.passthrough(),
|
|
114
|
+
getDesign: z
|
|
115
|
+
.object({
|
|
116
|
+
design: anyRecord.optional(),
|
|
117
|
+
})
|
|
118
|
+
.passthrough(),
|
|
119
|
+
createBrand: z
|
|
120
|
+
.object({
|
|
121
|
+
brand: anyRecord.optional(),
|
|
122
|
+
reviewStatus: z.string().optional(),
|
|
123
|
+
})
|
|
124
|
+
.passthrough(),
|
|
125
|
+
generate: z
|
|
126
|
+
.object({
|
|
127
|
+
designId: z.string(),
|
|
128
|
+
generationId: z.string(),
|
|
129
|
+
status: z.string(),
|
|
130
|
+
editorUrl: z.string(),
|
|
131
|
+
statusUrl: z.string(),
|
|
132
|
+
format: z.string().optional(),
|
|
133
|
+
formatNote: z.string().optional(),
|
|
134
|
+
credits: z.unknown().optional(),
|
|
135
|
+
})
|
|
136
|
+
.passthrough(),
|
|
137
|
+
poll: z
|
|
138
|
+
.object({
|
|
139
|
+
designId: z.string(),
|
|
140
|
+
generationId: z.string(),
|
|
141
|
+
status: z.string(),
|
|
142
|
+
editorUrl: z.string(),
|
|
143
|
+
imageUrl: z.string().optional(),
|
|
144
|
+
completedAt: z.string().nullable().optional(),
|
|
145
|
+
platformBadge: z.boolean().optional(),
|
|
146
|
+
badgeNote: z.string().optional(),
|
|
147
|
+
failureMessage: z.string().optional(),
|
|
148
|
+
})
|
|
149
|
+
.passthrough(),
|
|
150
|
+
download: z
|
|
151
|
+
.object({
|
|
152
|
+
saved: z.boolean(),
|
|
153
|
+
path: z.string(),
|
|
154
|
+
size: z.number(),
|
|
155
|
+
})
|
|
156
|
+
.passthrough(),
|
|
157
|
+
};
|
|
158
|
+
export const readOnly = {
|
|
159
|
+
readOnlyHint: true,
|
|
160
|
+
destructiveHint: false,
|
|
161
|
+
idempotentHint: true,
|
|
162
|
+
openWorldHint: true,
|
|
163
|
+
};
|
|
164
|
+
export const writeRemote = {
|
|
165
|
+
readOnlyHint: false,
|
|
166
|
+
destructiveHint: false,
|
|
167
|
+
idempotentHint: false,
|
|
168
|
+
openWorldHint: true,
|
|
169
|
+
};
|
|
170
|
+
export const writeLocal = {
|
|
171
|
+
readOnlyHint: false,
|
|
172
|
+
destructiveHint: true,
|
|
173
|
+
idempotentHint: false,
|
|
174
|
+
openWorldHint: true,
|
|
175
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { api, query } from "./api-client.js";
|
|
3
|
+
import { argsObject, outputSchemas, readOnly, safeIdSchema } from "./tool-schemas.js";
|
|
4
|
+
export const accountTools = [
|
|
5
|
+
{
|
|
6
|
+
name: "tday_get_credit_prices",
|
|
7
|
+
title: "Credit prices",
|
|
8
|
+
description: "What each thing costs in credits at today's price list: an image design, an edit, design videos, launch videos (every length, with or without music and voiceover, already added up), a launch video revision, scans, and each plan's monthly credits. Use it to tell the person what something will cost BEFORE starting it, next to the balance from tday_whoami.",
|
|
9
|
+
inputSchema: argsObject({}),
|
|
10
|
+
outputSchema: outputSchemas.creditPrices,
|
|
11
|
+
annotations: readOnly,
|
|
12
|
+
handler: (_args, config) => api(config, "/api/v1/credits/prices"),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "tday_get_usage",
|
|
16
|
+
title: "Credit usage",
|
|
17
|
+
description: "One organisation's credit balance and what it has spent this grant period, by action, by brand and by person, plus a link to its usage page. Pass organizationId from tday_whoami; omitted, the organisation the person owns.",
|
|
18
|
+
inputSchema: argsObject({
|
|
19
|
+
organizationId: safeIdSchema.optional().describe("Organisation ID from tday_whoami"),
|
|
20
|
+
}),
|
|
21
|
+
outputSchema: outputSchemas.usage,
|
|
22
|
+
annotations: readOnly,
|
|
23
|
+
handler: (args, config) => api(config, `/api/v1/usage${query({ organizationId: typeof args.organizationId === "string" ? args.organizationId : undefined })}`),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
export const IMAGE_FORMATS = ["square", "ig_post", "story", "fb_post", "linkedin_post", "desktop", "a4_page"];
|
|
27
|
+
export const imageFormatSchema = z
|
|
28
|
+
.enum(IMAGE_FORMATS)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Preset size: square 1080x1080, ig_post 1080x1350 (4:5 portrait), story 1080x1920 (9:16), fb_post 1200x630, linkedin_post 1200x627, desktop 1920x1080, a4_page. Portrait, square and story are on the paid plans where the plan gates sizes (tday_whoami output.allSizes); asking for one that is not included is refused with an upgrade link.");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { api, query } from "./api-client.js";
|
|
3
|
+
import { argsObject, readOnly, safeIdSchema } from "./tool-schemas.js";
|
|
4
|
+
const MAX_FEATURES = 50;
|
|
5
|
+
export const LIST_FEATURES_DESCRIPTION = "List a brand's features (its Launch page catalog), newest added first, with the videos each already has: how many launch videos are finished or being made, and the latest one with its link. " +
|
|
6
|
+
"Use it for the newest feature, for the features that still have no launch video (without: launch_video), or to find a feature's id for tday_create_launch_video. " +
|
|
7
|
+
"brandId may be left out when the account has one brand; otherwise pass one from tday_list_brands. `more` counts the features not listed. Spends no credits.";
|
|
8
|
+
const featureListSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
brand: z.object({ id: z.string(), name: z.string() }).passthrough(),
|
|
11
|
+
counts: z.record(z.unknown()),
|
|
12
|
+
features: z.array(z.record(z.unknown())),
|
|
13
|
+
more: z.number(),
|
|
14
|
+
})
|
|
15
|
+
.passthrough();
|
|
16
|
+
export const featureTools = [
|
|
17
|
+
{
|
|
18
|
+
name: "tday_list_features",
|
|
19
|
+
title: "List features",
|
|
20
|
+
description: LIST_FEATURES_DESCRIPTION,
|
|
21
|
+
inputSchema: argsObject({
|
|
22
|
+
brandId: safeIdSchema.optional().describe("Brand ID from tday_list_brands. Optional when the account has one brand."),
|
|
23
|
+
without: z
|
|
24
|
+
.enum(["launch_video"])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("launch_video: only features with no launch video finished or being made."),
|
|
27
|
+
search: z.string().trim().min(1).max(160).optional().describe("Only features whose title contains this, ignoring case."),
|
|
28
|
+
limit: z.number().int().min(1).max(MAX_FEATURES).default(20).describe(`How many to list, newest first (at most ${MAX_FEATURES}). Defaults to 20.`),
|
|
29
|
+
}),
|
|
30
|
+
outputSchema: featureListSchema,
|
|
31
|
+
annotations: readOnly,
|
|
32
|
+
handler: (args, config) => api(config, `/api/v1/features${query({
|
|
33
|
+
brandId: typeof args.brandId === "string" ? args.brandId : undefined,
|
|
34
|
+
without: typeof args.without === "string" ? args.without : undefined,
|
|
35
|
+
q: typeof args.search === "string" ? args.search : undefined,
|
|
36
|
+
limit: typeof args.limit === "number" ? args.limit : undefined,
|
|
37
|
+
})}`),
|
|
38
|
+
},
|
|
39
|
+
];
|