@tday-com/mcp 0.2.0 → 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.
Files changed (42) hide show
  1. package/README.md +25 -6
  2. package/dist/api-client.js +1 -18
  3. package/dist/cli.js +0 -3
  4. package/dist/config.js +0 -4
  5. package/dist/download-safety.js +2 -5
  6. package/dist/installer-agents.js +132 -216
  7. package/dist/installer-callback.js +0 -7
  8. package/dist/installer-slash-command.js +94 -35
  9. package/dist/installer.js +34 -11
  10. package/dist/package-info.js +14 -8
  11. package/dist/prompts.js +36 -0
  12. package/dist/result-format.js +7 -31
  13. package/dist/server.js +12 -6
  14. package/dist/tool-schemas.js +0 -14
  15. package/dist/tools-account.js +0 -4
  16. package/dist/tools-features.js +39 -0
  17. package/dist/tools-launch.js +43 -50
  18. package/dist/tools.js +5 -26
  19. package/package.json +4 -14
  20. package/dist/api-client.d.ts +0 -23
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/config.d.ts +0 -7
  23. package/dist/demo-import.d.ts +0 -6
  24. package/dist/demo-import.js +0 -101
  25. package/dist/download-safety.d.ts +0 -7
  26. package/dist/installer-agents.d.ts +0 -17
  27. package/dist/installer-callback.d.ts +0 -13
  28. package/dist/installer-slash-command.d.ts +0 -12
  29. package/dist/installer.d.ts +0 -2
  30. package/dist/observability.d.ts +0 -1
  31. package/dist/observability.js +0 -77
  32. package/dist/package-info.d.ts +0 -11
  33. package/dist/result-format.d.ts +0 -4
  34. package/dist/server.d.ts +0 -1
  35. package/dist/tool-schemas.d.ts +0 -460
  36. package/dist/tools-account.d.ts +0 -6
  37. package/dist/tools-demo.d.ts +0 -11
  38. package/dist/tools-demo.js +0 -57
  39. package/dist/tools-knowledge.d.ts +0 -4
  40. package/dist/tools-knowledge.js +0 -111
  41. package/dist/tools-launch.d.ts +0 -4
  42. package/dist/tools.d.ts +0 -15
@@ -1,13 +1,6 @@
1
- import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- const legacyCommandFilenames = [
5
- "markup.md",
6
- "markup-design.md",
7
- "markup-brand.md",
8
- "markup-credits.md",
9
- "markup-ideas.md",
10
- ];
11
4
  const commands = [
12
5
  {
13
6
  filename: "tday.md",
@@ -27,6 +20,7 @@ If the user asks something outside your scope (codebase questions, file operatio
27
20
  - \`tday_list_brands\` / \`tday_get_brand\` .. brand library
28
21
  - \`tday_create_brand\` .. create brand from a website URL
29
22
  - \`tday_list_designs\` / \`tday_get_design\` .. design history
23
+ - \`tday_list_features\` .. the brand's features, newest first, with the launch videos each has
30
24
  - \`tday_generate_design\` .. create a new design from a prompt
31
25
  - \`tday_poll_generation\` .. poll generation status until done
32
26
  - \`tday_download_image\` .. save a generated image to a local file
@@ -46,7 +40,7 @@ If the user asks something outside your scope (codebase questions, file operatio
46
40
 
47
41
  **STRICT RULES (violations break user trust):**
48
42
  - ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool.
49
- - NEVER mention: localhost, dev server, bun, npm, API keys, Gemini, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
43
+ - NEVER mention: localhost, dev server, bun, npm, API keys, AI model or vendor names, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
50
44
  - NEVER suggest: checking logs, restarting servers, debugging infrastructure, investigating code, or any developer action.
51
45
  - NEVER say: "stuck", "hanging", "slow", "taking longer than usual", "seems to be stuck", "might be an issue".
52
46
  - If a design is still generating: just say "Still creating your design..." and keep polling. That's it.
@@ -110,7 +104,7 @@ This is a focused workflow: resolve brand, generate, poll, deliver.
110
104
 
111
105
  **STRICT RULES (violations break user trust):**
112
106
  - ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool.
113
- - NEVER mention: localhost, dev server, bun, npm, API keys, Gemini, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
107
+ - NEVER mention: localhost, dev server, bun, npm, API keys, AI model or vendor names, workers, queues, pipelines, cold starts, rate limits, backend, rendering engine, or any implementation detail.
114
108
  - NEVER suggest: checking logs, restarting servers, debugging infrastructure, investigating code, or any developer action.
115
109
  - NEVER say: "stuck", "hanging", "slow", "taking longer than usual", "seems to be stuck", "might be an issue".
116
110
  - If a design is still generating: just say "Still creating your design..." and keep polling. Designs take 30-90 seconds. That is normal. Be patient.
@@ -211,18 +205,92 @@ You are a creative design strategist for tday.com. ONLY use tday_* MCP tools. Do
211
205
 
212
206
  **Tone:** Creative, specific, opinionated. You have taste. Don't suggest generic "professional marketing material". Every idea should feel like it was designed for this specific brand.
213
207
 
214
- **STRICT RULES:** ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention localhost, dev servers, API keys, Gemini, workers, queues, or any implementation detail. NEVER say "stuck", "hanging", "slow", or suggest debugging. If generation is in progress, just keep polling with a spinner line. If it fails, say "Generation didn't complete. Try a different prompt." You are the public face of tday.com.
208
+ **STRICT RULES:** ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention localhost, dev servers, API keys, AI model or vendor names, workers, queues, or any implementation detail. NEVER say "stuck", "hanging", "slow", or suggest debugging. If generation is in progress, just keep polling with a spinner line. If it fails, say "Generation didn't complete. Try a different prompt." You are the public face of tday.com.
209
+ `,
210
+ },
211
+ {
212
+ filename: "tday-launch-video.md",
213
+ needs: "launchVideos",
214
+ body: `---
215
+ name: tday-launch-video
216
+ description: Make a launch video in tday for a feature. Pass a feature name, or leave it empty for the newest feature without one.
217
+ ---
218
+
219
+ You are the tday.com launch video assistant. ONLY use tday_* MCP tools. Do NOT use Bash, Read, Glob, Grep, Write, Edit, or any local tools.
220
+
221
+ **Which feature ($ARGUMENTS):**
222
+ - A feature name: call \`tday_list_features\` with \`search\` set to it and use the match's id. If several match, show them and ask.
223
+ - Empty: call \`tday_list_features\` with \`without: "launch_video"\`. Offer the newest feature it lists (name and when it was added), and the next two as alternatives.
224
+ - If tday asks for a brand, call \`tday_list_brands\` and ask which one.
225
+
226
+ **Steps:**
227
+ 1. Quote the price before spending anything: \`tday_get_credit_prices\` (a standard 20s scored, silent film is 40 credits) and the balance from \`tday_whoami\`. Ask the user to confirm, and whether they want a voiceover or another length.
228
+ 2. Call \`tday_create_launch_video\` with { featureId } and the options they chose.
229
+ 3. Poll \`tday_poll_launch_video\` every 20-30 seconds, one short line per poll. A film takes about 2-5 minutes.
230
+ 4. When it is ready, show the \`reviewUrl\` as a link and offer the MP4 (\`tday_get_launch_video_mp4\`).
231
+
232
+ If tday says there are not enough credits or the plan does not include launch videos, show its sentence as it is, with the links it gives, and do not retry.
233
+
234
+ **STRICT RULES:** ONLY use tday_* MCP tools. NEVER use Bash, Read, Glob, Grep, Write, Edit, Agent, or any other tool. NEVER mention internal systems or implementation details. You are the public face of tday.com.
235
+ `,
236
+ },
237
+ {
238
+ filename: "tday-demo.md",
239
+ needs: "demos",
240
+ body: `---
241
+ name: tday-demo
242
+ description: Record a narrated demo video of a feature of this repo's app for tday. Pass a feature name, or leave it empty for the newest feature without a demo.
243
+ ---
244
+
245
+ Record a demo video for tday with the tday-capture MCP tools, from the app in this repo running on this machine. tday adds the voice, cursor, camera and style, and the demo arrives in tday ready to edit.
246
+
247
+ **Which feature ($ARGUMENTS):**
248
+ - A feature name or a tday feature id: call \`demo_features\` with \`search\` set to it and use the match's id.
249
+ - Empty: call \`demo_features\` with \`without: "demo"\`. Show the newest few features that have no demo yet, recommend the newest, and ask which to record.
250
+
251
+ **Steps:**
252
+ 1. Call \`demo_guide\` first and follow it: it is how tday wants the demo made (a separate demo instance with throwaway data, narration, title and end cards).
253
+ 2. Call \`demo_app_notes\` for how this product's apps were started before.
254
+ 3. Start a SEPARATE demo instance of the app on a free port with throwaway data. Never restart the developer's own server or touch their data.
255
+ 4. Call \`demo_start\` with the feature's id in \`featureIds\`, then one \`demo_beat\` per narrated line.
256
+ 5. Call \`demo_review\` and show the developer the review page. Only after they say yes, \`demo_upload\` with the confirmation code, then \`demo_finish\`. Share the links it returns.
257
+
258
+ If the tday-capture tools are not available, tell the developer to run \`curl -fsSL https://tday.com/install.sh | bash\` once and restart this session.
259
+ `,
260
+ },
261
+ {
262
+ filename: "tday-demos-needed.md",
263
+ needs: "demos",
264
+ body: `---
265
+ name: tday-demos-needed
266
+ description: List this product's features and docs pages that have no demo video yet in tday, then record the ones you pick.
267
+ ---
268
+
269
+ Use the tday-capture MCP tools.
270
+
271
+ 1. Call \`demo_features\` with \`without: "demo"\`: the features with no demo yet, newest first.
272
+ 2. Call \`demo_docs\`: the docs pages whose sections still need a demo. If tday says the product has no docs connected, skip this list quietly.
273
+ 3. Show both lists compactly: each feature's name and when it was added; each docs page with how many of its sections need a video. Ask which to record.
274
+ 4. For a feature, record it as /tday-demo does. For a docs page, follow what \`demo_docs\` says for that page. Run \`demo_features\` or \`demo_docs\` again at the end to show what is left.
275
+
276
+ If the tday-capture tools are not available, tell the developer to run \`curl -fsSL https://tday.com/install.sh | bash\` once and restart this session.
215
277
  `,
216
278
  },
217
279
  ];
218
- /** A command's text by filename, without installing anything (tests read it without touching ~/.claude). */
219
280
  export function slashCommandBody(filename) {
220
281
  return commands.find((command) => command.filename === filename)?.body;
221
282
  }
222
- export function installSlashCommands() {
223
- const claudeDir = join(homedir(), ".claude");
283
+ const commandName = (filename) => filename.replace(/\.md$/, "");
284
+ export function slashCommandsFor(access) {
285
+ return commands.filter((cmd) => !cmd.needs || access[cmd.needs]).map((cmd) => cmd.filename);
286
+ }
287
+ export function installSlashCommands(access = { demos: false, launchVideos: false }, home = homedir()) {
288
+ const wanted = new Set(slashCommandsFor(access));
289
+ const included = commands.filter((cmd) => wanted.has(cmd.filename));
290
+ const claudeDir = join(home, ".claude");
224
291
  if (!existsSync(claudeDir)) {
225
- return commands.map((c) => ({
292
+ return included.map((c) => ({
293
+ name: commandName(c.filename),
226
294
  path: join(claudeDir, "commands", c.filename),
227
295
  installed: false,
228
296
  }));
@@ -231,32 +299,23 @@ export function installSlashCommands() {
231
299
  if (!existsSync(commandsDir)) {
232
300
  mkdirSync(commandsDir, { recursive: true });
233
301
  }
234
- cleanupLegacySlashCommands(commandsDir);
235
- return commands.map((cmd) => {
236
- const path = join(commandsDir, cmd.filename);
237
- writeFileSync(path, cmd.body, { encoding: "utf8", mode: 0o644 });
238
- return { path, installed: true };
239
- });
240
- }
241
- export function cleanupLegacySlashCommands(commandsDir) {
242
- const removed = [];
243
- for (const filename of legacyCommandFilenames) {
244
- const path = join(commandsDir, filename);
302
+ for (const cmd of commands) {
303
+ if (wanted.has(cmd.filename))
304
+ continue;
245
305
  try {
246
- if (existsSync(path)) {
247
- unlinkSync(path);
248
- removed.push(path);
249
- }
306
+ rmSync(join(commandsDir, cmd.filename), { force: true });
250
307
  }
251
308
  catch {
252
- // Best-effort cleanup. Installing current tday commands should still proceed.
253
309
  }
254
310
  }
255
- return removed;
311
+ return included.map((cmd) => {
312
+ const path = join(commandsDir, cmd.filename);
313
+ writeFileSync(path, cmd.body, { encoding: "utf8", mode: 0o644 });
314
+ return { name: commandName(cmd.filename), path, installed: true };
315
+ });
256
316
  }
257
- // Backward-compat alias for installer.ts
258
- export function installSlashCommand() {
259
- const results = installSlashCommands();
317
+ export function installSlashCommand(home = homedir()) {
318
+ const results = installSlashCommands(undefined, home);
260
319
  const firstInstalled = results.find((r) => r.installed);
261
320
  return firstInstalled ?? { path: results[0]?.path ?? "", installed: false };
262
321
  }
package/dist/installer.js CHANGED
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
6
6
  import { startCallbackServer } from "./installer-callback.js";
7
7
  import { patchAllAgents, previewAllAgents } from "./installer-agents.js";
8
8
  import { installSlashCommands } from "./installer-slash-command.js";
9
+ import { capturePublished } from "./package-info.js";
9
10
  const WEB_URL = (process.env.TDAY_WEB_URL ?? "https://tday.com").replace(/\/+$/, "");
10
11
  const API_URL = (process.env.TDAY_API_URL ?? WEB_URL).replace(/\/+$/, "").replace(/\/api$/i, "");
11
12
  const TIMEOUT_MS = 5 * 60 * 1000;
@@ -15,7 +16,7 @@ export async function runInstaller(args) {
15
16
  console.log("tday MCP installer\n");
16
17
  if (dryRun) {
17
18
  console.log("Dry run - no files will be written, no browser opened.\n");
18
- const results = previewAllAgents();
19
+ const results = previewAllAgents({ capture: true });
19
20
  for (const r of results) {
20
21
  console.log(` [${r.action}] ${r.name} - ${r.message}`);
21
22
  }
@@ -56,22 +57,44 @@ export async function runInstaller(args) {
56
57
  }
57
58
  saveConfig(token);
58
59
  console.log(` Saved secure local connection to ${configPath()} (chmod 0600)\n`);
59
- console.log("5. Patching installed AI agents (Claude Code, Cursor, Gemini CLI, Codex)…");
60
- const results = patchAllAgents();
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 });
61
63
  for (const r of results) {
62
64
  const tag = r.action === "patched" ? "+" : r.action === "manual" ? "!" : "-";
63
65
  console.log(` ${tag} ${r.name}: ${r.message}`);
64
66
  }
65
67
  console.log("\n6. Installing Claude Code slash commands...");
66
- const cmds = installSlashCommands();
67
- for (const cmd of cmds) {
68
- console.log(cmd.installed ? ` + ${cmd.path}` : ` - Skipped (Claude Code not found)`);
69
- }
70
- console.log(`\nDone. Restart your AI agent. In Claude Code, type / to see: /tday, /tday-design, /tday-brand, /tday-credits, /tday-ideas`);
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
+ };
71
97
  }
72
- // --------------------------------------------------------------------------
73
- // helpers
74
- // --------------------------------------------------------------------------
75
98
  function configPath() {
76
99
  return join(homedir(), ".tday", "config.json");
77
100
  }
@@ -1,17 +1,23 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  export const PACKAGE_NAME = "@tday-com/mcp";
3
- /** This build's own version, read from the package.json it shipped in (dist/ and src/ both sit one level down). */
4
3
  export function packageVersion() {
5
4
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6
5
  return pkg.version;
7
6
  }
8
- /**
9
- * What an agent's MCP config launches: this exact version, not whatever `latest` is on the day
10
- * the agent next starts. `npx -y <name>` re-resolves on every launch, so a bad publish (a
11
- * compromised maintainer account, a mistaken tag) would reach every installed agent, and the
12
- * token in ~/.tday/config.json with it, without anyone running anything. Pinned, an update only
13
- * arrives when someone re-runs `install`, which writes the version it was itself run as.
14
- */
15
7
  export function pinnedPackageSpec() {
16
8
  return `${PACKAGE_NAME}@${packageVersion()}`;
17
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
+ }
@@ -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
+ }
@@ -1,5 +1,3 @@
1
- // Tool-result presentation: JSON stringification, structuredContent shaping,
2
- // and per-tool markdown rendering.
3
1
  const MAX_TEXT_CHARS = 25_000;
4
2
  export function stringifyResult(result) {
5
3
  return JSON.stringify(result, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2);
@@ -7,8 +5,8 @@ export function stringifyResult(result) {
7
5
  export function toStructuredContent(result) {
8
6
  return isRecord(result) ? result : { result };
9
7
  }
10
- export function formatToolResultText(toolName, result, format) {
11
- const text = format === "markdown" ? markdownResult(toolName, result) : stringifyResult(result);
8
+ export function formatToolResultText(toolName, result, format, markdown) {
9
+ const text = format === "markdown" ? markdownResult(toolName, result, markdown) : stringifyResult(result);
12
10
  return truncateText(text);
13
11
  }
14
12
  function isRecord(value) {
@@ -19,9 +17,11 @@ function truncateText(text) {
19
17
  return text;
20
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.]`;
21
19
  }
22
- function markdownResult(toolName, result) {
20
+ function markdownResult(toolName, result, markdown) {
23
21
  if (!isRecord(result))
24
22
  return String(result);
23
+ if (markdown)
24
+ return markdown(result);
25
25
  switch (toolName) {
26
26
  case "tday_whoami":
27
27
  return markdownWhoami(result);
@@ -49,22 +49,6 @@ function markdownResult(toolName, result) {
49
49
  result.editorUrl ? `- Editor: ${result.editorUrl}` : undefined,
50
50
  result.failureMessage ? `- Failure: ${result.failureMessage}` : undefined,
51
51
  ].filter(Boolean).join("\n");
52
- case "tday_search_knowledge_entries":
53
- return markdownList("Knowledge entries", result.entries, [
54
- "slug",
55
- "type",
56
- "draft",
57
- "verified",
58
- "description",
59
- ]);
60
- case "tday_create_knowledge_entry":
61
- return [
62
- result.created ? "# Knowledge entry recorded" : "# Knowledge entry updated",
63
- `- Slug: ${display(result.slug)}`,
64
- `- Graph node: ${display(result.node)}`,
65
- "",
66
- "Recorded as a draft - it appears in the knowledge graph immediately, marked unreviewed.",
67
- ].join("\n");
68
52
  case "tday_download_image":
69
53
  return [
70
54
  "# Image downloaded",
@@ -88,18 +72,11 @@ function markdownWhoami(result) {
88
72
  balanceLine(result),
89
73
  ].join("\n");
90
74
  }
91
- /**
92
- * The balance line. Credits when the account has them (`credits.enabled`);
93
- * otherwise the prepaid dollar balance an account carried before credits,
94
- * which is what `/api/billing/info` reports until credits reach the account.
95
- * Every number is the API's own: nothing here knows a price or a grant.
96
- */
97
75
  function balanceLine(result) {
98
76
  const credits = isRecord(result.credits) ? result.credits : null;
99
77
  if (credits && credits.enabled === true) {
100
78
  if (typeof credits.balance !== "number")
101
79
  return `- Credits: ${display(credits.balance)}`;
102
- // An uncapped contract has no grant to count down from, and is never refused.
103
80
  if (credits.grant === null)
104
81
  return "- Credits: usage is covered by your contract";
105
82
  const grant = typeof credits.grant === "number" ? ` of ${formatCount(credits.grant)}` : "";
@@ -116,14 +93,13 @@ function balanceLine(result) {
116
93
  function formatCount(value) {
117
94
  return Math.trunc(value).toLocaleString("en-US");
118
95
  }
119
- /** "Oct 1", in UTC like the app's own refusal copy. The raw value when it is not a date. */
120
96
  function formatDay(iso) {
121
97
  const date = new Date(iso);
122
98
  if (Number.isNaN(date.getTime()))
123
99
  return iso;
124
100
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
125
101
  }
126
- function markdownList(title, value, columns) {
102
+ export function markdownList(title, value, columns) {
127
103
  const items = Array.isArray(value) ? value.filter(isRecord) : [];
128
104
  const lines = [`# ${title}`, ""];
129
105
  if (items.length === 0) {
@@ -138,7 +114,7 @@ function markdownList(title, value, columns) {
138
114
  }
139
115
  return lines.join("\n");
140
116
  }
141
- function display(value) {
117
+ export function display(value) {
142
118
  if (value === null || value === undefined || value === "")
143
119
  return "-";
144
120
  if (typeof value === "string" ||
package/dist/server.js CHANGED
@@ -1,12 +1,20 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { loadConfig } from "./config.js";
4
- import { listedTools, formatToolResultText, responseFormatFromArgs, toStructuredContent, } from "./tools.js";
4
+ import { packageVersion } from "./package-info.js";
5
+ import { registerPrompts } from "./prompts.js";
6
+ import { tools, formatToolResultText, responseFormatFromArgs, toStructuredContent, } from "./tools.js";
5
7
  export async function runServer() {
6
8
  const config = await loadConfig();
7
- const server = new McpServer({ name: "tday-mcp-server", version: "0.0.1" });
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) {
8
16
  const registerTool = server.registerTool.bind(server);
9
- for (const tool of await listedTools(config)) {
17
+ for (const tool of list) {
10
18
  registerTool(tool.name, {
11
19
  title: tool.title,
12
20
  description: tool.description,
@@ -25,13 +33,11 @@ export async function runServer() {
25
33
  content: [
26
34
  {
27
35
  type: "text",
28
- text: formatToolResultText(tool.name, result, responseFormatFromArgs(rawArgs)),
36
+ text: formatToolResultText(tool.name, result, responseFormatFromArgs(rawArgs), tool.markdown),
29
37
  },
30
38
  ],
31
39
  structuredContent: toStructuredContent(result),
32
40
  };
33
41
  });
34
42
  }
35
- const transport = new StdioServerTransport();
36
- await server.connect(transport);
37
43
  }
@@ -67,7 +67,6 @@ export const outputSchemas = {
67
67
  whoami: z
68
68
  .object({
69
69
  account: z.unknown().optional(),
70
- staffExempt: z.boolean().optional(),
71
70
  organizations: z.array(anyRecord).optional(),
72
71
  defaultOrganizationId: z.string().nullable().optional(),
73
72
  features: z.unknown().optional(),
@@ -155,19 +154,6 @@ export const outputSchemas = {
155
154
  size: z.number(),
156
155
  })
157
156
  .passthrough(),
158
- searchKnowledgeEntries: z
159
- .object({
160
- entries: z.array(anyRecord),
161
- count: z.number().optional(),
162
- })
163
- .passthrough(),
164
- createKnowledgeEntry: z
165
- .object({
166
- slug: z.string(),
167
- created: z.boolean(),
168
- node: z.string(),
169
- })
170
- .passthrough(),
171
157
  };
172
158
  export const readOnly = {
173
159
  readOnlyHint: true,
@@ -1,9 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { api, query } from "./api-client.js";
3
3
  import { argsObject, outputSchemas, readOnly, safeIdSchema } from "./tool-schemas.js";
4
- // What things cost and what an organisation has spent. `tday_whoami` (tools.ts) is the account
5
- // itself; these two sit beside it. The hosted connector registers the same three
6
- // (web/src/lib/mcp/remote-account-tools.ts), so keep the descriptions in step.
7
4
  export const accountTools = [
8
5
  {
9
6
  name: "tday_get_credit_prices",
@@ -26,7 +23,6 @@ export const accountTools = [
26
23
  handler: (args, config) => api(config, `/api/v1/usage${query({ organizationId: typeof args.organizationId === "string" ? args.organizationId : undefined })}`),
27
24
  },
28
25
  ];
29
- /** The preset sizes `tday_generate_design` accepts (web/src/lib/mcp/v1-image-formats.ts). */
30
26
  export const IMAGE_FORMATS = ["square", "ig_post", "story", "fb_post", "linkedin_post", "desktop", "a4_page"];
31
27
  export const imageFormatSchema = z
32
28
  .enum(IMAGE_FORMATS)
@@ -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
+ ];