@tday-com/mcp 0.0.0-stage → 0.2.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 (41) hide show
  1. package/LICENSE +36 -0
  2. package/README.md +105 -2
  3. package/dist/api-client.d.ts +23 -0
  4. package/dist/api-client.js +187 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +78 -0
  7. package/dist/config.d.ts +7 -0
  8. package/dist/config.js +66 -0
  9. package/dist/demo-import.d.ts +6 -0
  10. package/dist/demo-import.js +101 -0
  11. package/dist/download-safety.d.ts +7 -0
  12. package/dist/download-safety.js +76 -0
  13. package/dist/installer-agents.d.ts +17 -0
  14. package/dist/installer-agents.js +283 -0
  15. package/dist/installer-callback.d.ts +13 -0
  16. package/dist/installer-callback.js +91 -0
  17. package/dist/installer-slash-command.d.ts +12 -0
  18. package/dist/installer-slash-command.js +262 -0
  19. package/dist/installer.d.ts +2 -0
  20. package/dist/installer.js +127 -0
  21. package/dist/observability.d.ts +1 -0
  22. package/dist/observability.js +77 -0
  23. package/dist/package-info.d.ts +11 -0
  24. package/dist/package-info.js +17 -0
  25. package/dist/result-format.d.ts +4 -0
  26. package/dist/result-format.js +154 -0
  27. package/dist/server.d.ts +1 -0
  28. package/dist/server.js +37 -0
  29. package/dist/tool-schemas.d.ts +460 -0
  30. package/dist/tool-schemas.js +189 -0
  31. package/dist/tools-account.d.ts +6 -0
  32. package/dist/tools-account.js +34 -0
  33. package/dist/tools-demo.d.ts +11 -0
  34. package/dist/tools-demo.js +57 -0
  35. package/dist/tools-knowledge.d.ts +4 -0
  36. package/dist/tools-knowledge.js +111 -0
  37. package/dist/tools-launch.d.ts +4 -0
  38. package/dist/tools-launch.js +365 -0
  39. package/dist/tools.d.ts +15 -0
  40. package/dist/tools.js +296 -0
  41. package/package.json +58 -4
@@ -0,0 +1,76 @@
1
+ import { lstat, mkdir, realpath } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve, relative, sep } from "node:path";
3
+ // Download safety: URL allowlist + path sandboxing + size cap for
4
+ // tday_download_image (see tools.ts).
5
+ export const ALLOWED_IMAGE_HOSTS = new Set([
6
+ "cdn.tday.com",
7
+ "video-cdn.communities.one", // pre-2026 stored URLs
8
+ "communities.one",
9
+ "media.brand.dev",
10
+ "asset.brandfetch.io",
11
+ "cdn.brandfetch.io",
12
+ "tday.com",
13
+ "www.tday.com",
14
+ "app.tday.com",
15
+ ]);
16
+ export const MAX_IMAGE_BYTES = 50 * 1024 * 1024; // 50 MB
17
+ export function validateImageUrl(raw) {
18
+ let url;
19
+ try {
20
+ url = new URL(raw);
21
+ }
22
+ catch {
23
+ throw new Error(`Invalid image URL: "${raw}"`);
24
+ }
25
+ if (url.protocol !== "https:") {
26
+ throw new Error(`Image URL must use HTTPS (got ${url.protocol})`);
27
+ }
28
+ if (!ALLOWED_IMAGE_HOSTS.has(url.hostname)) {
29
+ throw new Error(`Image host "${url.hostname}" is not allowed. Expected one of: ${[...ALLOWED_IMAGE_HOSTS].join(", ")}`);
30
+ }
31
+ return url;
32
+ }
33
+ export function sandboxFilePath(raw) {
34
+ const cwd = process.cwd();
35
+ const resolved = resolve(cwd, raw);
36
+ const rel = relative(cwd, resolved);
37
+ // Match ".." only as a full path segment — a file literally named "..foo" is inside cwd.
38
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel) || resolve(cwd, rel) !== resolved) {
39
+ throw new Error(`File path must be inside the working directory. "${raw}" resolves outside of ${cwd}`);
40
+ }
41
+ return resolved;
42
+ }
43
+ export async function prepareOutputFilePath(raw, options = {}) {
44
+ const filePath = sandboxFilePath(raw);
45
+ const parentDir = dirname(filePath);
46
+ await mkdir(parentDir, { recursive: true });
47
+ const cwdReal = await realpath(process.cwd());
48
+ const parentReal = await realpath(parentDir);
49
+ if (!isInsideDirectory(cwdReal, parentReal)) {
50
+ throw new Error(`File path parent directory must stay inside the working directory and cannot traverse symlinks outside of ${process.cwd()}`);
51
+ }
52
+ try {
53
+ const existing = await lstat(filePath);
54
+ if (existing.isSymbolicLink()) {
55
+ throw new Error("Refusing to write to a symbolic link.");
56
+ }
57
+ if (!existing.isFile()) {
58
+ throw new Error("Refusing to write because the target path exists and is not a regular file.");
59
+ }
60
+ if (!options.overwrite) {
61
+ throw new Error("File already exists. Choose a new filePath or pass overwrite: true.");
62
+ }
63
+ }
64
+ catch (err) {
65
+ if (!isNodeError(err) || err.code !== "ENOENT")
66
+ throw err;
67
+ }
68
+ return filePath;
69
+ }
70
+ function isInsideDirectory(base, target) {
71
+ const rel = relative(base, target);
72
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
73
+ }
74
+ function isNodeError(err) {
75
+ return err instanceof Error && "code" in err;
76
+ }
@@ -0,0 +1,17 @@
1
+ export type AgentId = "claude-code" | "cursor" | "gemini-cli" | "codex";
2
+ export interface AgentPatchResult {
3
+ id: AgentId;
4
+ name: string;
5
+ action: "patched" | "skipped" | "manual";
6
+ configPath?: string;
7
+ message: string;
8
+ }
9
+ interface McpServerEntry {
10
+ command: string;
11
+ args: string[];
12
+ env?: Record<string, string>;
13
+ }
14
+ export declare function mcpCommand(): McpServerEntry;
15
+ export declare function patchAllAgents(): AgentPatchResult[];
16
+ export declare function previewAllAgents(): AgentPatchResult[];
17
+ export {};
@@ -0,0 +1,283 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, dirname } from "node:path";
4
+ import { pinnedPackageSpec } from "./package-info.js";
5
+ const SERVER_NAME = "tday";
6
+ // Command used inside each agent's MCP config. Uses npx -y so the user
7
+ // doesn't need the package installed globally; -y skips the prompt. The
8
+ // version is pinned to the installer's own (see pinnedPackageSpec).
9
+ export function mcpCommand() {
10
+ return {
11
+ command: "npx",
12
+ args: ["-y", pinnedPackageSpec(), "serve"],
13
+ };
14
+ }
15
+ // --------------------------------------------------------------------------
16
+ // Claude Code — JSON at ~/.claude.json
17
+ // --------------------------------------------------------------------------
18
+ function patchClaudeCode() {
19
+ const configPath = join(homedir(), ".claude.json");
20
+ if (!existsSync(configPath)) {
21
+ return {
22
+ id: "claude-code",
23
+ name: "Claude Code",
24
+ action: "skipped",
25
+ configPath,
26
+ message: `No Claude Code config found at ${configPath}. Install Claude Code first, then re-run \`tday install\`.`,
27
+ };
28
+ }
29
+ let raw;
30
+ try {
31
+ raw = readFileSync(configPath, "utf8");
32
+ }
33
+ catch (err) {
34
+ return {
35
+ id: "claude-code",
36
+ name: "Claude Code",
37
+ action: "skipped",
38
+ configPath,
39
+ message: `Could not read ${configPath}: ${err.message}`,
40
+ };
41
+ }
42
+ let config;
43
+ try {
44
+ config = raw.trim() ? JSON.parse(raw) : {};
45
+ }
46
+ catch (err) {
47
+ return {
48
+ id: "claude-code",
49
+ name: "Claude Code",
50
+ action: "skipped",
51
+ configPath,
52
+ message: `Existing ${configPath} is not valid JSON (${err.message}). Fix it or patch manually.`,
53
+ };
54
+ }
55
+ const mcpServers = getMcpServers(config);
56
+ mcpServers[SERVER_NAME] = mcpCommand();
57
+ config.mcpServers = mcpServers;
58
+ writeFileAtomic(configPath, JSON.stringify(config, null, 2) + "\n");
59
+ return {
60
+ id: "claude-code",
61
+ name: "Claude Code",
62
+ action: "patched",
63
+ configPath,
64
+ message: `Added "${SERVER_NAME}" MCP server to ${configPath}.`,
65
+ };
66
+ }
67
+ // --------------------------------------------------------------------------
68
+ // Cursor — JSON at ~/.cursor/mcp.json
69
+ // --------------------------------------------------------------------------
70
+ function patchCursor() {
71
+ const cursorDir = join(homedir(), ".cursor");
72
+ const configPath = join(cursorDir, "mcp.json");
73
+ if (!existsSync(cursorDir)) {
74
+ return {
75
+ id: "cursor",
76
+ name: "Cursor",
77
+ action: "skipped",
78
+ configPath,
79
+ message: `No Cursor install detected (${cursorDir} missing).`,
80
+ };
81
+ }
82
+ let config = {};
83
+ if (existsSync(configPath)) {
84
+ try {
85
+ const raw = readFileSync(configPath, "utf8");
86
+ config = raw.trim() ? JSON.parse(raw) : {};
87
+ }
88
+ catch (err) {
89
+ return {
90
+ id: "cursor",
91
+ name: "Cursor",
92
+ action: "skipped",
93
+ configPath,
94
+ message: `Existing ${configPath} is not valid JSON (${err.message}).`,
95
+ };
96
+ }
97
+ }
98
+ const mcpServers = getMcpServers(config);
99
+ mcpServers[SERVER_NAME] = mcpCommand();
100
+ config.mcpServers = mcpServers;
101
+ writeFileAtomic(configPath, JSON.stringify(config, null, 2) + "\n");
102
+ return {
103
+ id: "cursor",
104
+ name: "Cursor",
105
+ action: "patched",
106
+ configPath,
107
+ message: `Added "${SERVER_NAME}" MCP server to ${configPath}.`,
108
+ };
109
+ }
110
+ // --------------------------------------------------------------------------
111
+ // Gemini CLI — JSON at ~/.gemini/settings.json
112
+ // Gemini CLI follows the common MCP config shape with a top-level mcpServers
113
+ // object, matching Cursor/Claude-style entries.
114
+ // --------------------------------------------------------------------------
115
+ function patchGeminiCli() {
116
+ const geminiDir = join(homedir(), ".gemini");
117
+ const configPath = join(geminiDir, "settings.json");
118
+ if (!existsSync(geminiDir)) {
119
+ return {
120
+ id: "gemini-cli",
121
+ name: "Gemini CLI",
122
+ action: "skipped",
123
+ configPath,
124
+ message: `No Gemini CLI config directory detected (${geminiDir} missing).`,
125
+ };
126
+ }
127
+ let config = {};
128
+ if (existsSync(configPath)) {
129
+ try {
130
+ const raw = readFileSync(configPath, "utf8");
131
+ config = raw.trim() ? JSON.parse(raw) : {};
132
+ }
133
+ catch (err) {
134
+ return {
135
+ id: "gemini-cli",
136
+ name: "Gemini CLI",
137
+ action: "skipped",
138
+ configPath,
139
+ message: `Existing ${configPath} is not valid JSON (${err.message}).`,
140
+ };
141
+ }
142
+ }
143
+ const mcpServers = getMcpServers(config);
144
+ mcpServers[SERVER_NAME] = mcpCommand();
145
+ config.mcpServers = mcpServers;
146
+ writeFileAtomic(configPath, JSON.stringify(config, null, 2) + "\n");
147
+ return {
148
+ id: "gemini-cli",
149
+ name: "Gemini CLI",
150
+ action: "patched",
151
+ configPath,
152
+ message: `Added "${SERVER_NAME}" MCP server to ${configPath}.`,
153
+ };
154
+ }
155
+ // --------------------------------------------------------------------------
156
+ // Codex — TOML at ~/.codex/config.toml
157
+ // We avoid parsing TOML to keep zero runtime deps; instead, print the
158
+ // block so the user can paste it in. Safer than corrupting a user's file.
159
+ // --------------------------------------------------------------------------
160
+ function patchCodex() {
161
+ const codexDir = join(homedir(), ".codex");
162
+ const configPath = join(codexDir, "config.toml");
163
+ const block = [
164
+ ``,
165
+ `[mcp_servers."${SERVER_NAME}"]`,
166
+ `command = "npx"`,
167
+ `args = ["-y", "${pinnedPackageSpec()}", "serve"]`,
168
+ ``,
169
+ ].join("\n");
170
+ if (!existsSync(codexDir)) {
171
+ return {
172
+ id: "codex",
173
+ name: "Codex",
174
+ action: "skipped",
175
+ configPath,
176
+ message: `No Codex install detected (${codexDir} missing).`,
177
+ };
178
+ }
179
+ return {
180
+ id: "codex",
181
+ name: "Codex",
182
+ action: "manual",
183
+ configPath,
184
+ message: `Append the following block to ${configPath}:\n${block}\n` +
185
+ `(Codex uses TOML; we don't auto-edit it to avoid corrupting your config.)`,
186
+ };
187
+ }
188
+ // --------------------------------------------------------------------------
189
+ // Runner
190
+ // --------------------------------------------------------------------------
191
+ export function patchAllAgents() {
192
+ return [patchClaudeCode(), patchCursor(), patchGeminiCli(), patchCodex()];
193
+ }
194
+ // Preview what patchAllAgents would do without touching the filesystem.
195
+ // Used by `tday install --dry-run`.
196
+ export function previewAllAgents() {
197
+ const home = homedir();
198
+ const claudePath = join(home, ".claude.json");
199
+ const cursorPath = join(home, ".cursor", "mcp.json");
200
+ const geminiPath = join(home, ".gemini", "settings.json");
201
+ const codexPath = join(home, ".codex", "config.toml");
202
+ return [
203
+ existsSync(claudePath)
204
+ ? {
205
+ id: "claude-code",
206
+ name: "Claude Code",
207
+ action: "patched",
208
+ configPath: claudePath,
209
+ message: `Would add "${SERVER_NAME}" MCP server to ${claudePath}.`,
210
+ }
211
+ : {
212
+ id: "claude-code",
213
+ name: "Claude Code",
214
+ action: "skipped",
215
+ configPath: claudePath,
216
+ message: `No Claude Code config at ${claudePath}.`,
217
+ },
218
+ existsSync(join(home, ".cursor"))
219
+ ? {
220
+ id: "cursor",
221
+ name: "Cursor",
222
+ action: "patched",
223
+ configPath: cursorPath,
224
+ message: `Would add "${SERVER_NAME}" MCP server to ${cursorPath}.`,
225
+ }
226
+ : {
227
+ id: "cursor",
228
+ name: "Cursor",
229
+ action: "skipped",
230
+ configPath: cursorPath,
231
+ message: `No Cursor install detected.`,
232
+ },
233
+ existsSync(join(home, ".gemini"))
234
+ ? {
235
+ id: "gemini-cli",
236
+ name: "Gemini CLI",
237
+ action: "patched",
238
+ configPath: geminiPath,
239
+ message: `Would add "${SERVER_NAME}" MCP server to ${geminiPath}.`,
240
+ }
241
+ : {
242
+ id: "gemini-cli",
243
+ name: "Gemini CLI",
244
+ action: "skipped",
245
+ configPath: geminiPath,
246
+ message: `No Gemini CLI config directory detected.`,
247
+ },
248
+ existsSync(join(home, ".codex"))
249
+ ? {
250
+ id: "codex",
251
+ name: "Codex",
252
+ action: "manual",
253
+ configPath: codexPath,
254
+ message: `Would print TOML block for ${codexPath} (manual append).`,
255
+ }
256
+ : {
257
+ id: "codex",
258
+ name: "Codex",
259
+ action: "skipped",
260
+ configPath: codexPath,
261
+ message: `No Codex install detected.`,
262
+ },
263
+ ];
264
+ }
265
+ function getMcpServers(config) {
266
+ const existing = config.mcpServers;
267
+ if (isPlainRecord(existing)) {
268
+ return existing;
269
+ }
270
+ return {};
271
+ }
272
+ function isPlainRecord(value) {
273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
274
+ }
275
+ function writeFileAtomic(path, contents) {
276
+ const dir = dirname(path);
277
+ if (!existsSync(dir)) {
278
+ mkdirSync(dir, { recursive: true });
279
+ }
280
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
281
+ writeFileSync(tmp, contents, { encoding: "utf8", mode: 0o600 });
282
+ renameSync(tmp, path);
283
+ }
@@ -0,0 +1,13 @@
1
+ interface CallbackResult {
2
+ token: string;
3
+ state: string;
4
+ }
5
+ interface WaitOptions {
6
+ expectedState: string;
7
+ timeoutMs: number;
8
+ }
9
+ export declare function startCallbackServer(options: WaitOptions): Promise<{
10
+ port: number;
11
+ done: Promise<CallbackResult>;
12
+ }>;
13
+ export {};
@@ -0,0 +1,91 @@
1
+ import http from "node:http";
2
+ // Boots a loopback HTTP server on a random free port, resolves once the
3
+ // /cli/auth browser page returns its authorization payload, and rejects on
4
+ // timeout or malformed payloads. Binds 127.0.0.1 only so no remote host can
5
+ // reach it.
6
+ export async function startCallbackServer(options) {
7
+ let resolve;
8
+ let reject;
9
+ const done = new Promise((res, rej) => {
10
+ resolve = res;
11
+ reject = rej;
12
+ });
13
+ const server = http.createServer((req, res) => {
14
+ res.setHeader("access-control-allow-origin", "*");
15
+ res.setHeader("access-control-allow-methods", "POST, OPTIONS");
16
+ res.setHeader("access-control-allow-headers", "content-type");
17
+ if (req.method === "OPTIONS") {
18
+ res.statusCode = 204;
19
+ res.end();
20
+ return;
21
+ }
22
+ if (req.url && new URL(req.url, "http://127.0.0.1").pathname !== "/cb") {
23
+ res.statusCode = 404;
24
+ res.end("not found");
25
+ return;
26
+ }
27
+ if (req.method !== "POST") {
28
+ res.statusCode = 405;
29
+ res.end("method not allowed");
30
+ return;
31
+ }
32
+ let body = "";
33
+ req.on("data", (chunk) => {
34
+ body += chunk.toString();
35
+ if (body.length > 8_192) {
36
+ res.statusCode = 413;
37
+ res.end("payload too large");
38
+ req.destroy();
39
+ }
40
+ });
41
+ req.on("end", () => {
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(body);
45
+ }
46
+ catch {
47
+ res.statusCode = 400;
48
+ res.end("invalid json");
49
+ return;
50
+ }
51
+ if (!parsed ||
52
+ typeof parsed !== "object" ||
53
+ typeof parsed.token !== "string" ||
54
+ typeof parsed.state !== "string") {
55
+ res.statusCode = 400;
56
+ res.end("missing authorization payload or state");
57
+ return;
58
+ }
59
+ const payload = parsed;
60
+ if (payload.state !== options.expectedState) {
61
+ // Do not reject the whole installer on a bad local POST. A different
62
+ // local process can hit the loopback listener, so keep waiting for the
63
+ // browser page carrying the expected nonce until timeout.
64
+ res.statusCode = 400;
65
+ res.end("state mismatch");
66
+ return;
67
+ }
68
+ res.statusCode = 200;
69
+ res.setHeader("content-type", "application/json");
70
+ res.end(JSON.stringify({ ok: true }));
71
+ resolve(payload);
72
+ });
73
+ });
74
+ await new Promise((res, rej) => {
75
+ server.once("error", rej);
76
+ server.listen(0, "127.0.0.1", () => res());
77
+ });
78
+ const address = server.address();
79
+ if (!address || typeof address === "string") {
80
+ server.close();
81
+ throw new Error("failed to bind callback server");
82
+ }
83
+ const timer = setTimeout(() => {
84
+ reject(new Error(`Authorization timed out after ${options.timeoutMs / 1000}s`));
85
+ }, options.timeoutMs);
86
+ done.finally(() => {
87
+ clearTimeout(timer);
88
+ server.close();
89
+ });
90
+ return { port: address.port, done };
91
+ }
@@ -0,0 +1,12 @@
1
+ export interface SlashCommandResult {
2
+ path: string;
3
+ installed: boolean;
4
+ }
5
+ /** A command's text by filename, without installing anything (tests read it without touching ~/.claude). */
6
+ export declare function slashCommandBody(filename: string): string | undefined;
7
+ export declare function installSlashCommands(): SlashCommandResult[];
8
+ export declare function cleanupLegacySlashCommands(commandsDir: string): string[];
9
+ export declare function installSlashCommand(): {
10
+ path: string;
11
+ installed: boolean;
12
+ };