busy-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +81 -0
  2. package/dist/busy-api.d.ts +47 -0
  3. package/dist/busy-api.js +114 -0
  4. package/dist/config.d.ts +17 -0
  5. package/dist/config.js +44 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +95 -0
  8. package/dist/setup/config-store.d.ts +15 -0
  9. package/dist/setup/config-store.js +35 -0
  10. package/dist/setup/index.d.ts +8 -0
  11. package/dist/setup/index.js +7 -0
  12. package/dist/setup/tools/complete.d.ts +10 -0
  13. package/dist/setup/tools/complete.js +69 -0
  14. package/dist/setup/tools/find-user.d.ts +10 -0
  15. package/dist/setup/tools/find-user.js +26 -0
  16. package/dist/setup/tools/list-available-projects.d.ts +4 -0
  17. package/dist/setup/tools/list-available-projects.js +27 -0
  18. package/dist/setup/tools/select-projects.d.ts +25 -0
  19. package/dist/setup/tools/select-projects.js +45 -0
  20. package/dist/setup/tools/set-user-id.d.ts +10 -0
  21. package/dist/setup/tools/set-user-id.js +17 -0
  22. package/dist/setup/tools/start.d.ts +4 -0
  23. package/dist/setup/tools/start.js +29 -0
  24. package/dist/tools/create-entry.d.ts +22 -0
  25. package/dist/tools/create-entry.js +48 -0
  26. package/dist/tools/delete-entry.d.ts +10 -0
  27. package/dist/tools/delete-entry.js +18 -0
  28. package/dist/tools/get-entries.d.ts +13 -0
  29. package/dist/tools/get-entries.js +39 -0
  30. package/dist/tools/list-projects.d.ts +1 -0
  31. package/dist/tools/list-projects.js +23 -0
  32. package/dist/tools/reconfigure.d.ts +10 -0
  33. package/dist/tools/reconfigure.js +12 -0
  34. package/dist/tools/update-entry.d.ts +22 -0
  35. package/dist/tools/update-entry.js +54 -0
  36. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Busy MCP Server
2
+
3
+ An MCP (Model Context Protocol) server for [Busy](https://busy.no) time tracking. Log hours with natural language through Claude.
4
+
5
+ ## Installation
6
+
7
+ ### Via npx (recommended)
8
+
9
+ ```bash
10
+ npx busy-mcp-server
11
+ ```
12
+
13
+ ### Via npm
14
+
15
+ ```bash
16
+ npm install -g busy-mcp-server
17
+ ```
18
+
19
+ ## Setup
20
+
21
+ ### 1. Get your Busy API token
22
+
23
+ 1. Log in to [Busy](https://app.busy.no)
24
+ 2. Go to profile settings → API
25
+ 3. Generate an API token
26
+
27
+ ### 2. Configure Claude Code
28
+
29
+ Add to your Claude Code MCP settings (`~/.claude/settings.json`):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "busy": {
35
+ "command": "npx",
36
+ "args": ["busy-mcp-server"],
37
+ "env": {
38
+ "BUSY_API_TOKEN": "your-token-here"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### 3. First-time setup
46
+
47
+ When you first use the server, Claude will guide you through setup:
48
+
49
+ 1. **User ID** - Found via email lookup or in your Busy URL: `app.busy.no/timeline/{org}/users/{YOUR_ID}`
50
+ 2. **Projects** - Select which projects you work on
51
+ 3. **Aliases** - Add shortcuts like "novem" for "Cube - Novem - Retainer"
52
+
53
+ Configuration is saved to `~/.config/busy-mcp/config.json`.
54
+
55
+ ## Usage
56
+
57
+ Once configured, you can:
58
+
59
+ - **Log time:** "Log 2 hours to novem for code review"
60
+ - **Check entries:** "What did I log today?"
61
+ - **Update entries:** "Change that last entry to 3 hours"
62
+ - **Delete entries:** "Delete entry 12345"
63
+
64
+ ## Tools
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `create_time_entry` | Log hours to a project |
69
+ | `get_entries` | View time entries for a date range |
70
+ | `list_projects` | Show configured projects and tags |
71
+ | `update_entry` | Modify an existing entry |
72
+ | `delete_entry` | Remove an entry |
73
+ | `reconfigure` | Reset and run setup again |
74
+
75
+ ## Limitations
76
+
77
+ - **Recurring entries** cannot be modified via the API. Edit them in the Busy web app.
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,47 @@
1
+ export interface HourEntry {
2
+ id: string;
3
+ userId: string;
4
+ projectId: string;
5
+ tagId: string;
6
+ billableMinutes: number;
7
+ totalMinutes: number;
8
+ description: string;
9
+ startTime: string;
10
+ stopTime: string;
11
+ }
12
+ export interface User {
13
+ id: string;
14
+ email: string;
15
+ firstName: string;
16
+ lastName: string;
17
+ }
18
+ export interface Project {
19
+ id: string;
20
+ name: string;
21
+ }
22
+ export interface Tag {
23
+ id: string;
24
+ name: string;
25
+ }
26
+ export interface CreateEntryParams {
27
+ projectId: number;
28
+ totalMinutes: number;
29
+ tagId: number;
30
+ description?: string;
31
+ date?: string;
32
+ }
33
+ export interface UpdateEntryParams {
34
+ projectId?: number;
35
+ totalMinutes?: number;
36
+ tagId?: number;
37
+ description?: string;
38
+ }
39
+ export declare function getUsers(): Promise<User[]>;
40
+ export declare function getProjects(): Promise<Project[]>;
41
+ export declare function getTags(): Promise<Tag[]>;
42
+ export declare function createHourEntry(params: CreateEntryParams & {
43
+ userId: string;
44
+ }): Promise<HourEntry>;
45
+ export declare function getHourEntries(dateFrom?: string, dateTo?: string, userId?: string): Promise<HourEntry[]>;
46
+ export declare function updateHourEntry(entryId: string, params: UpdateEntryParams): Promise<HourEntry>;
47
+ export declare function deleteHourEntry(entryId: string): Promise<void>;
@@ -0,0 +1,114 @@
1
+ import { BUSY_API_BASE_URL } from "./config.js";
2
+ function getToken() {
3
+ const token = process.env.BUSY_API_TOKEN;
4
+ if (!token) {
5
+ throw new Error("BUSY_API_TOKEN environment variable is not set. " +
6
+ "Please set it in your shell profile or Claude Code MCP config.");
7
+ }
8
+ return token;
9
+ }
10
+ async function request(endpoint, options = {}) {
11
+ const token = getToken();
12
+ const response = await fetch(`${BUSY_API_BASE_URL}${endpoint}`, {
13
+ ...options,
14
+ headers: {
15
+ "Authorization": `Bearer ${token}`,
16
+ "Content-Type": "application/json",
17
+ ...options.headers,
18
+ },
19
+ });
20
+ if (!response.ok) {
21
+ const errorBody = await response.text();
22
+ throw new Error(`Busy API error (${response.status}): ${errorBody}`);
23
+ }
24
+ return response.json();
25
+ }
26
+ export async function getUsers() {
27
+ const response = await request("/users");
28
+ return response.data;
29
+ }
30
+ export async function getProjects() {
31
+ const response = await request("/projects");
32
+ return response.data;
33
+ }
34
+ export async function getTags() {
35
+ const response = await request("/tags");
36
+ return response.data;
37
+ }
38
+ export async function createHourEntry(params) {
39
+ const date = params.date || new Date().toISOString().split("T")[0];
40
+ // Create start and stop times for the given date
41
+ // Default: 8am to 8am + hours
42
+ const hours = params.totalMinutes / 60;
43
+ const startHour = 8;
44
+ const endHour = startHour + hours;
45
+ const startTime = `${date}T${String(startHour).padStart(2, "0")}:00:00.000Z`;
46
+ const stopTime = `${date}T${String(Math.floor(endHour)).padStart(2, "0")}:${String(Math.round((endHour % 1) * 60)).padStart(2, "0")}:00.000Z`;
47
+ const body = {
48
+ userId: params.userId,
49
+ projectId: String(params.projectId),
50
+ startTime,
51
+ stopTime,
52
+ totalMinutes: params.totalMinutes,
53
+ billableMinutes: params.totalMinutes,
54
+ tagId: String(params.tagId),
55
+ description: params.description || "",
56
+ externalId: null,
57
+ };
58
+ const response = await request("/hourEntries", {
59
+ method: "POST",
60
+ body: JSON.stringify(body),
61
+ });
62
+ return response.data;
63
+ }
64
+ export async function getHourEntries(dateFrom, dateTo, userId) {
65
+ if (!userId) {
66
+ throw new Error("userId is required");
67
+ }
68
+ const today = new Date().toISOString().split("T")[0];
69
+ const from = dateFrom || today;
70
+ const to = dateTo || today;
71
+ const params = new URLSearchParams({
72
+ startTimeFrom: from,
73
+ startTimeTo: to,
74
+ userIdIn: userId,
75
+ limit: "100",
76
+ });
77
+ const response = await request(`/hourEntries/?${params.toString()}`);
78
+ return response.data;
79
+ }
80
+ export async function updateHourEntry(entryId, params) {
81
+ const body = {};
82
+ if (params.projectId !== undefined) {
83
+ body.projectId = String(params.projectId);
84
+ }
85
+ if (params.totalMinutes !== undefined) {
86
+ body.totalMinutes = params.totalMinutes;
87
+ body.billableMinutes = params.totalMinutes;
88
+ // Recalculate stop time if we have new minutes
89
+ // This is a simplification - in reality we'd want to fetch the entry first
90
+ const hours = params.totalMinutes / 60;
91
+ const startHour = 8;
92
+ const endHour = startHour + hours;
93
+ const today = new Date().toISOString().split("T")[0];
94
+ body.startTime = `${today}T${String(startHour).padStart(2, "0")}:00:00.000Z`;
95
+ body.stopTime = `${today}T${String(Math.floor(endHour)).padStart(2, "0")}:${String(Math.round((endHour % 1) * 60)).padStart(2, "0")}:00.000Z`;
96
+ }
97
+ if (params.tagId !== undefined) {
98
+ body.tagId = String(params.tagId);
99
+ }
100
+ if (params.description !== undefined) {
101
+ body.description = params.description;
102
+ }
103
+ const response = await request(`/hourEntries/${entryId}`, {
104
+ method: "PATCH",
105
+ body: JSON.stringify(body),
106
+ });
107
+ return response.data;
108
+ }
109
+ export async function deleteHourEntry(entryId) {
110
+ await request(`/hourEntries/${entryId}`, {
111
+ method: "PATCH",
112
+ body: JSON.stringify({ isActive: false }),
113
+ });
114
+ }
@@ -0,0 +1,17 @@
1
+ import { type ProjectConfig } from "./setup/config-store.js";
2
+ export declare const BUSY_API_BASE_URL = "https://api.busy.no/v2";
3
+ export declare const USER_ID = 169169;
4
+ export interface Project {
5
+ id: number;
6
+ name: string;
7
+ aliases: string[];
8
+ }
9
+ export interface Tag {
10
+ id: number;
11
+ name: string;
12
+ }
13
+ export declare function getConfiguredProjects(): ProjectConfig[];
14
+ export declare function getUserId(): string | null;
15
+ export declare function getDefaultTagId(): string | null;
16
+ export declare function findProject(query: string): ProjectConfig | undefined;
17
+ export declare function getProjectList(): string;
package/dist/config.js ADDED
@@ -0,0 +1,44 @@
1
+ import { loadConfig } from "./setup/config-store.js";
2
+ export const BUSY_API_BASE_URL = "https://api.busy.no/v2";
3
+ // Legacy USER_ID for backwards compatibility during migration
4
+ export const USER_ID = 169169;
5
+ // These are now loaded from config, with fallback to empty
6
+ export function getConfiguredProjects() {
7
+ const config = loadConfig();
8
+ return config?.projects || [];
9
+ }
10
+ export function getUserId() {
11
+ const config = loadConfig();
12
+ return config?.userId || null;
13
+ }
14
+ export function getDefaultTagId() {
15
+ const config = loadConfig();
16
+ return config?.defaultTagId || null;
17
+ }
18
+ export function findProject(query) {
19
+ const projects = getConfiguredProjects();
20
+ const q = query.toLowerCase().trim();
21
+ // Exact match on name
22
+ const exactMatch = projects.find((p) => p.name.toLowerCase() === q);
23
+ if (exactMatch)
24
+ return exactMatch;
25
+ // Match on alias
26
+ const aliasMatch = projects.find((p) => p.aliases.some((a) => a.toLowerCase() === q));
27
+ if (aliasMatch)
28
+ return aliasMatch;
29
+ // Partial match on name or alias
30
+ const partialMatch = projects.find((p) => p.name.toLowerCase().includes(q) ||
31
+ p.aliases.some((a) => a.toLowerCase().includes(q)));
32
+ if (partialMatch)
33
+ return partialMatch;
34
+ return undefined;
35
+ }
36
+ export function getProjectList() {
37
+ const projects = getConfiguredProjects();
38
+ if (projects.length === 0) {
39
+ return "No projects configured. Run setup first.";
40
+ }
41
+ return projects
42
+ .map((p) => `- ${p.name} (aliases: ${p.aliases.join(", ") || "none"})`)
43
+ .join("\n");
44
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { configExists, setupStart, setupStartSchema, setupFindUser, setupFindUserSchema, setupSetUserId, setupSetUserIdSchema, setupListAvailableProjects, setupListProjectsSchema, setupSelectProjects, setupSelectProjectsSchema, setupComplete, setupCompleteSchema, } from "./setup/index.js";
5
+ import { createEntry, createEntrySchema } from "./tools/create-entry.js";
6
+ import { listProjects } from "./tools/list-projects.js";
7
+ import { getEntries, getEntriesSchema } from "./tools/get-entries.js";
8
+ import { updateEntry, updateEntrySchema } from "./tools/update-entry.js";
9
+ import { deleteEntry, deleteEntrySchema } from "./tools/delete-entry.js";
10
+ import { reconfigure, reconfigureSchema } from "./tools/reconfigure.js";
11
+ const server = new McpServer({
12
+ name: "busy-time-tracker",
13
+ version: "1.0.0",
14
+ });
15
+ function registerSetupTools() {
16
+ server.tool("setup_start", "Start the Busy MCP setup process. Checks for API token and provides instructions.", setupStartSchema.shape, async () => {
17
+ const result = await setupStart();
18
+ return { content: [{ type: "text", text: result }] };
19
+ });
20
+ server.tool("setup_find_user_by_email", "Look up your Busy user ID by email address", setupFindUserSchema.shape, async (args) => {
21
+ const input = setupFindUserSchema.parse(args);
22
+ const result = await setupFindUser(input);
23
+ return { content: [{ type: "text", text: result }] };
24
+ });
25
+ server.tool("setup_set_user_id", "Set your Busy user ID (from email lookup or timeline URL)", setupSetUserIdSchema.shape, async (args) => {
26
+ const input = setupSetUserIdSchema.parse(args);
27
+ const result = await setupSetUserId(input);
28
+ return { content: [{ type: "text", text: result }] };
29
+ });
30
+ server.tool("setup_list_available_projects", "List all projects available in your Busy account", setupListProjectsSchema.shape, async () => {
31
+ const result = await setupListAvailableProjects();
32
+ return { content: [{ type: "text", text: result }] };
33
+ });
34
+ server.tool("setup_select_projects", "Select which projects you want to use and optionally add aliases", setupSelectProjectsSchema.shape, async (args) => {
35
+ const input = setupSelectProjectsSchema.parse(args);
36
+ const result = await setupSelectProjects(input);
37
+ return { content: [{ type: "text", text: result }] };
38
+ });
39
+ server.tool("setup_complete", "Complete the setup process and save configuration", setupCompleteSchema.shape, async (args) => {
40
+ const input = setupCompleteSchema.parse(args);
41
+ const result = await setupComplete(input);
42
+ return { content: [{ type: "text", text: result }] };
43
+ });
44
+ }
45
+ function registerNormalTools() {
46
+ server.tool("create_time_entry", "Log hours to a project in Busy. Accepts fuzzy project names like 'novem' or 'internal'.", createEntrySchema.shape, async (args) => {
47
+ const input = createEntrySchema.parse(args);
48
+ const result = await createEntry(input);
49
+ return { content: [{ type: "text", text: result }] };
50
+ });
51
+ server.tool("list_projects", "List all available projects and tags in Busy", {}, async () => {
52
+ const result = await listProjects();
53
+ return { content: [{ type: "text", text: result }] };
54
+ });
55
+ server.tool("get_entries", "Get time entries for a date range. Defaults to today if no dates specified.", getEntriesSchema.shape, async (args) => {
56
+ const input = getEntriesSchema.parse(args);
57
+ const result = await getEntries(input);
58
+ return { content: [{ type: "text", text: result }] };
59
+ });
60
+ server.tool("update_entry", "Update an existing time entry. Can change hours, description, project, or tag.", updateEntrySchema.shape, async (args) => {
61
+ const input = updateEntrySchema.parse(args);
62
+ const result = await updateEntry(input);
63
+ return { content: [{ type: "text", text: result }] };
64
+ });
65
+ server.tool("delete_entry", "Delete a time entry by its ID", deleteEntrySchema.shape, async (args) => {
66
+ const input = deleteEntrySchema.parse(args);
67
+ const result = await deleteEntry(input);
68
+ return { content: [{ type: "text", text: result }] };
69
+ });
70
+ server.tool("reconfigure", "Reset configuration and run setup again", reconfigureSchema.shape, async (args) => {
71
+ const input = reconfigureSchema.parse(args);
72
+ const result = await reconfigure(input);
73
+ return { content: [{ type: "text", text: result }] };
74
+ });
75
+ }
76
+ async function main() {
77
+ const isConfigured = configExists();
78
+ if (isConfigured) {
79
+ // Normal operation: register all normal tools + reconfigure
80
+ registerNormalTools();
81
+ console.error("Busy MCP server running (configured)");
82
+ }
83
+ else {
84
+ // Setup mode: register setup tools + limited normal tools for seamless transition
85
+ registerSetupTools();
86
+ registerNormalTools(); // Also register normal tools so they work after setup completes
87
+ console.error("Busy MCP server running (setup mode - no config found)");
88
+ }
89
+ const transport = new StdioServerTransport();
90
+ await server.connect(transport);
91
+ }
92
+ main().catch((error) => {
93
+ console.error("Fatal error:", error);
94
+ process.exit(1);
95
+ });
@@ -0,0 +1,15 @@
1
+ export interface ProjectConfig {
2
+ id: string;
3
+ name: string;
4
+ aliases: string[];
5
+ }
6
+ export interface Config {
7
+ userId: string;
8
+ projects: ProjectConfig[];
9
+ defaultTagId: string;
10
+ }
11
+ export declare function getConfigPath(): string;
12
+ export declare function configExists(): boolean;
13
+ export declare function loadConfig(): Config | null;
14
+ export declare function saveConfig(config: Config): void;
15
+ export declare function deleteConfig(): void;
@@ -0,0 +1,35 @@
1
+ import envPaths from "env-paths";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
3
+ import { join } from "path";
4
+ const paths = envPaths("busy-mcp", { suffix: "" });
5
+ export function getConfigPath() {
6
+ return join(paths.config, "config.json");
7
+ }
8
+ export function configExists() {
9
+ return existsSync(getConfigPath());
10
+ }
11
+ export function loadConfig() {
12
+ if (!configExists()) {
13
+ return null;
14
+ }
15
+ try {
16
+ const content = readFileSync(getConfigPath(), "utf-8");
17
+ return JSON.parse(content);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function saveConfig(config) {
24
+ const configDir = paths.config;
25
+ if (!existsSync(configDir)) {
26
+ mkdirSync(configDir, { recursive: true });
27
+ }
28
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
29
+ }
30
+ export function deleteConfig() {
31
+ const configPath = getConfigPath();
32
+ if (existsSync(configPath)) {
33
+ unlinkSync(configPath);
34
+ }
35
+ }
@@ -0,0 +1,8 @@
1
+ export { setupStart, setupStartSchema } from "./tools/start.js";
2
+ export { setupFindUser, setupFindUserSchema } from "./tools/find-user.js";
3
+ export { setupSetUserId, setupSetUserIdSchema } from "./tools/set-user-id.js";
4
+ export { setupListAvailableProjects, setupListProjectsSchema } from "./tools/list-available-projects.js";
5
+ export { setupSelectProjects, setupSelectProjectsSchema } from "./tools/select-projects.js";
6
+ export { setupComplete, setupCompleteSchema } from "./tools/complete.js";
7
+ export { loadConfig, configExists, saveConfig, deleteConfig, getConfigPath } from "./config-store.js";
8
+ export type { Config, ProjectConfig } from "./config-store.js";
@@ -0,0 +1,7 @@
1
+ export { setupStart, setupStartSchema } from "./tools/start.js";
2
+ export { setupFindUser, setupFindUserSchema } from "./tools/find-user.js";
3
+ export { setupSetUserId, setupSetUserIdSchema } from "./tools/set-user-id.js";
4
+ export { setupListAvailableProjects, setupListProjectsSchema } from "./tools/list-available-projects.js";
5
+ export { setupSelectProjects, setupSelectProjectsSchema } from "./tools/select-projects.js";
6
+ export { setupComplete, setupCompleteSchema } from "./tools/complete.js";
7
+ export { loadConfig, configExists, saveConfig, deleteConfig, getConfigPath } from "./config-store.js";
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const setupCompleteSchema: z.ZodObject<{
3
+ default_tag: z.ZodOptional<z.ZodString>;
4
+ }, "strip", z.ZodTypeAny, {
5
+ default_tag?: string | undefined;
6
+ }, {
7
+ default_tag?: string | undefined;
8
+ }>;
9
+ export type SetupCompleteInput = z.infer<typeof setupCompleteSchema>;
10
+ export declare function setupComplete(input: SetupCompleteInput): Promise<string>;
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { loadConfig, saveConfig, getConfigPath } from "../config-store.js";
3
+ import { getTags } from "../../busy-api.js";
4
+ export const setupCompleteSchema = z.object({
5
+ default_tag: z.string().optional().describe("Default tag name (e.g., 'Working hours'). If not specified, uses first available tag."),
6
+ });
7
+ export async function setupComplete(input) {
8
+ const config = loadConfig();
9
+ if (!config || !config.userId) {
10
+ return "Setup incomplete: User ID not set. Use setup_set_user_id first.";
11
+ }
12
+ if (config.projects.length === 0) {
13
+ return "Setup incomplete: No projects selected. Use setup_select_projects to add at least one project.";
14
+ }
15
+ try {
16
+ const tags = await getTags();
17
+ if (tags.length === 0) {
18
+ return "Warning: No tags found in your Busy account. Using empty default.";
19
+ }
20
+ let defaultTag = tags[0];
21
+ if (input.default_tag) {
22
+ const matchedTag = tags.find((t) => t.name.toLowerCase() === input.default_tag.toLowerCase());
23
+ if (matchedTag) {
24
+ defaultTag = matchedTag;
25
+ }
26
+ }
27
+ else {
28
+ // Try to find "Working hours" as default
29
+ const workingHours = tags.find((t) => t.name.toLowerCase() === "working hours");
30
+ if (workingHours) {
31
+ defaultTag = workingHours;
32
+ }
33
+ }
34
+ config.defaultTagId = defaultTag.id;
35
+ saveConfig(config);
36
+ const projectList = config.projects
37
+ .map((p) => {
38
+ const aliasText = p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
39
+ return `- ${p.name}${aliasText}`;
40
+ })
41
+ .join("\n");
42
+ return `## Setup Complete!
43
+
44
+ **Configuration saved to:** \`${getConfigPath()}\`
45
+
46
+ **User ID:** ${config.userId}
47
+
48
+ **Projects:**
49
+ ${projectList}
50
+
51
+ **Default tag:** ${defaultTag.name}
52
+
53
+ ---
54
+
55
+ You can now use the Busy time tracking tools:
56
+ - \`create_time_entry\` - Log hours
57
+ - \`get_entries\` - View your time entries
58
+ - \`list_projects\` - See your configured projects
59
+ - \`update_entry\` - Modify entries
60
+ - \`delete_entry\` - Remove entries
61
+
62
+ To reconfigure later, use the \`reconfigure\` tool.
63
+
64
+ **Note:** Recurring time entries cannot be modified via this integration. Use the Busy web app for those.`;
65
+ }
66
+ catch (error) {
67
+ return `Error completing setup: ${error instanceof Error ? error.message : "Unknown error"}`;
68
+ }
69
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const setupFindUserSchema: z.ZodObject<{
3
+ email: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ email: string;
6
+ }, {
7
+ email: string;
8
+ }>;
9
+ export type SetupFindUserInput = z.infer<typeof setupFindUserSchema>;
10
+ export declare function setupFindUser(input: SetupFindUserInput): Promise<string>;
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { getUsers } from "../../busy-api.js";
3
+ export const setupFindUserSchema = z.object({
4
+ email: z.string().email().describe("Your email address used in Busy"),
5
+ });
6
+ export async function setupFindUser(input) {
7
+ try {
8
+ const users = await getUsers();
9
+ const user = users.find((u) => u.email.toLowerCase() === input.email.toLowerCase());
10
+ if (!user) {
11
+ const similarUsers = users.filter((u) => u.email.toLowerCase().includes(input.email.split("@")[0].toLowerCase()));
12
+ if (similarUsers.length > 0) {
13
+ const suggestions = similarUsers
14
+ .slice(0, 5)
15
+ .map((u) => `- ${u.firstName} ${u.lastName} (${u.email}) - ID: ${u.id}`)
16
+ .join("\n");
17
+ return `No exact match for "${input.email}". Similar users found:\n\n${suggestions}\n\nUse setup_set_user_id with the correct ID.`;
18
+ }
19
+ return `No user found with email "${input.email}". Please check the email or use setup_set_user_id to enter your ID manually from your Busy timeline URL.`;
20
+ }
21
+ return `Found user: **${user.firstName} ${user.lastName}** (${user.email})\n\nUser ID: **${user.id}**\n\nIs this correct? If yes, use setup_set_user_id with ID "${user.id}".`;
22
+ }
23
+ catch (error) {
24
+ return `Error looking up user: ${error instanceof Error ? error.message : "Unknown error"}. Please use setup_set_user_id to enter your ID manually.`;
25
+ }
26
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export declare const setupListProjectsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
3
+ export type SetupListProjectsInput = z.infer<typeof setupListProjectsSchema>;
4
+ export declare function setupListAvailableProjects(): Promise<string>;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { getProjects } from "../../busy-api.js";
3
+ export const setupListProjectsSchema = z.object({});
4
+ export async function setupListAvailableProjects() {
5
+ try {
6
+ const projects = await getProjects();
7
+ if (projects.length === 0) {
8
+ return "No projects found. Please check your API token has access to projects.";
9
+ }
10
+ const projectList = projects
11
+ .map((p) => `- **${p.name}** (ID: ${p.id})`)
12
+ .join("\n");
13
+ return `## Available Projects
14
+
15
+ ${projectList}
16
+
17
+ ---
18
+
19
+ Use setup_select_projects to choose which projects you want to use. You can also add aliases for quick access (e.g., "novem" for "Cube - Novem - Retainer").
20
+
21
+ Example: setup_select_projects with projects like:
22
+ \`[{"id": "123", "aliases": ["shortname"]}]\``;
23
+ }
24
+ catch (error) {
25
+ return `Error fetching projects: ${error instanceof Error ? error.message : "Unknown error"}`;
26
+ }
27
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ export declare const setupSelectProjectsSchema: z.ZodObject<{
3
+ projects: z.ZodArray<z.ZodObject<{
4
+ id: z.ZodString;
5
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ id: string;
8
+ aliases?: string[] | undefined;
9
+ }, {
10
+ id: string;
11
+ aliases?: string[] | undefined;
12
+ }>, "many">;
13
+ }, "strip", z.ZodTypeAny, {
14
+ projects: {
15
+ id: string;
16
+ aliases?: string[] | undefined;
17
+ }[];
18
+ }, {
19
+ projects: {
20
+ id: string;
21
+ aliases?: string[] | undefined;
22
+ }[];
23
+ }>;
24
+ export type SetupSelectProjectsInput = z.infer<typeof setupSelectProjectsSchema>;
25
+ export declare function setupSelectProjects(input: SetupSelectProjectsInput): Promise<string>;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { loadConfig, saveConfig } from "../config-store.js";
3
+ import { getProjects } from "../../busy-api.js";
4
+ const projectSelectionSchema = z.object({
5
+ id: z.string().describe("Project ID"),
6
+ aliases: z.array(z.string()).optional().describe("Short names for quick access"),
7
+ });
8
+ export const setupSelectProjectsSchema = z.object({
9
+ projects: z
10
+ .array(projectSelectionSchema)
11
+ .describe("Array of projects to add with optional aliases"),
12
+ });
13
+ export async function setupSelectProjects(input) {
14
+ const config = loadConfig();
15
+ if (!config || !config.userId) {
16
+ return "Please set your user ID first using setup_set_user_id.";
17
+ }
18
+ try {
19
+ const allProjects = await getProjects();
20
+ const selectedProjects = [];
21
+ for (const selection of input.projects) {
22
+ const project = allProjects.find((p) => p.id === selection.id);
23
+ if (!project) {
24
+ return `Project with ID "${selection.id}" not found. Use setup_list_available_projects to see valid IDs.`;
25
+ }
26
+ selectedProjects.push({
27
+ id: project.id,
28
+ name: project.name,
29
+ aliases: selection.aliases || [],
30
+ });
31
+ }
32
+ config.projects = [...config.projects, ...selectedProjects];
33
+ saveConfig(config);
34
+ const projectSummary = selectedProjects
35
+ .map((p) => {
36
+ const aliasText = p.aliases.length > 0 ? ` (aliases: ${p.aliases.join(", ")})` : "";
37
+ return `- ${p.name}${aliasText}`;
38
+ })
39
+ .join("\n");
40
+ return `Added ${selectedProjects.length} project(s):\n\n${projectSummary}\n\nTotal projects configured: ${config.projects.length}\n\nAdd more projects or use setup_complete to finish setup.`;
41
+ }
42
+ catch (error) {
43
+ return `Error selecting projects: ${error instanceof Error ? error.message : "Unknown error"}`;
44
+ }
45
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const setupSetUserIdSchema: z.ZodObject<{
3
+ user_id: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ user_id: string;
6
+ }, {
7
+ user_id: string;
8
+ }>;
9
+ export type SetupSetUserIdInput = z.infer<typeof setupSetUserIdSchema>;
10
+ export declare function setupSetUserId(input: SetupSetUserIdInput): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { loadConfig, saveConfig } from "../config-store.js";
3
+ export const setupSetUserIdSchema = z.object({
4
+ user_id: z.string().describe("Your Busy user ID (found in timeline URL or from email lookup)"),
5
+ });
6
+ export async function setupSetUserId(input) {
7
+ const existingConfig = loadConfig();
8
+ const config = {
9
+ userId: input.user_id,
10
+ projects: existingConfig?.projects || [],
11
+ defaultTagId: existingConfig?.defaultTagId || "",
12
+ };
13
+ saveConfig(config);
14
+ return `User ID set to: **${input.user_id}**
15
+
16
+ Next step: Let's select your projects. Use setup_list_available_projects to see all available projects.`;
17
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export declare const setupStartSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
3
+ export type SetupStartInput = z.infer<typeof setupStartSchema>;
4
+ export declare function setupStart(): Promise<string>;
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ export const setupStartSchema = z.object({});
3
+ export async function setupStart() {
4
+ const token = process.env.BUSY_API_TOKEN;
5
+ if (!token) {
6
+ return `## Busy MCP Setup
7
+
8
+ Welcome! Let's set up your Busy time tracking integration.
9
+
10
+ **Step 1: Get your API token**
11
+
12
+ 1. Log in to Busy at https://app.busy.no
13
+ 2. Go to your profile settings
14
+ 3. Find the API section and generate a token
15
+ 4. Set it as an environment variable: \`BUSY_API_TOKEN=your-token-here\`
16
+
17
+ Once you have the token set, run this setup again.`;
18
+ }
19
+ return `## Busy MCP Setup
20
+
21
+ API token found! Now we need your user ID.
22
+
23
+ **Two options:**
24
+
25
+ 1. **Find by email** - Tell me your email and I'll look up your user ID
26
+ 2. **Enter manually** - Find your ID in the Busy URL: \`https://app.busy.no/timeline/{org}/users/{YOUR_ID}\`
27
+
28
+ Which would you prefer?`;
29
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ export declare const createEntrySchema: z.ZodObject<{
3
+ project: z.ZodString;
4
+ hours: z.ZodNumber;
5
+ description: z.ZodOptional<z.ZodString>;
6
+ tag: z.ZodOptional<z.ZodString>;
7
+ date: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ project: string;
10
+ hours: number;
11
+ description?: string | undefined;
12
+ tag?: string | undefined;
13
+ date?: string | undefined;
14
+ }, {
15
+ project: string;
16
+ hours: number;
17
+ description?: string | undefined;
18
+ tag?: string | undefined;
19
+ date?: string | undefined;
20
+ }>;
21
+ export type CreateEntryInput = z.infer<typeof createEntrySchema>;
22
+ export declare function createEntry(input: CreateEntryInput): Promise<string>;
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ import { createHourEntry, getTags } from "../busy-api.js";
3
+ import { findProject, getProjectList, getUserId, getDefaultTagId } from "../config.js";
4
+ export const createEntrySchema = z.object({
5
+ project: z.string().describe("Project name or alias (e.g., 'novem', 'internal', 'dnb')"),
6
+ hours: z.number().positive().describe("Number of hours to log"),
7
+ description: z.string().optional().describe("Description of the work done"),
8
+ tag: z.string().optional().describe("Tag name (e.g., 'Development', 'Design'). Defaults to configured default tag"),
9
+ date: z.string().optional().describe("Date in YYYY-MM-DD format. Defaults to today"),
10
+ });
11
+ export async function createEntry(input) {
12
+ const userId = getUserId();
13
+ if (!userId) {
14
+ return "Not configured. Please run setup first.";
15
+ }
16
+ const project = findProject(input.project);
17
+ if (!project) {
18
+ return `Could not find project matching "${input.project}". Available projects:\n${getProjectList()}`;
19
+ }
20
+ let tagId = getDefaultTagId();
21
+ if (input.tag) {
22
+ try {
23
+ const tags = await getTags();
24
+ const tag = tags.find((t) => t.name.toLowerCase().includes(input.tag.toLowerCase()));
25
+ if (tag) {
26
+ tagId = tag.id;
27
+ }
28
+ else {
29
+ return `Could not find tag matching "${input.tag}".`;
30
+ }
31
+ }
32
+ catch {
33
+ return `Error fetching tags. Using default tag.`;
34
+ }
35
+ }
36
+ if (!tagId) {
37
+ return "No default tag configured. Please run setup again.";
38
+ }
39
+ const entry = await createHourEntry({
40
+ projectId: Number(project.id),
41
+ totalMinutes: Math.round(input.hours * 60),
42
+ tagId: Number(tagId),
43
+ description: input.description,
44
+ date: input.date,
45
+ userId,
46
+ });
47
+ return `Created time entry:\n- Project: ${project.name}\n- Hours: ${input.hours}\n- Description: ${input.description || "(none)"}\n- Entry ID: ${entry.id}`;
48
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const deleteEntrySchema: z.ZodObject<{
3
+ entry_id: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ entry_id: string;
6
+ }, {
7
+ entry_id: string;
8
+ }>;
9
+ export type DeleteEntryInput = z.infer<typeof deleteEntrySchema>;
10
+ export declare function deleteEntry(input: DeleteEntryInput): Promise<string>;
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { deleteHourEntry } from "../busy-api.js";
3
+ export const deleteEntrySchema = z.object({
4
+ entry_id: z.string().describe("The ID of the entry to delete"),
5
+ });
6
+ export async function deleteEntry(input) {
7
+ try {
8
+ await deleteHourEntry(input.entry_id);
9
+ return `Deleted entry ${input.entry_id}`;
10
+ }
11
+ catch (error) {
12
+ const message = error instanceof Error ? error.message : "Unknown error";
13
+ if (message.includes("recurring")) {
14
+ return `Cannot delete entry ${input.entry_id}: This is a recurring entry and cannot be deleted via the API. Please remove it in the Busy web app at https://app.busy.no`;
15
+ }
16
+ return `Error deleting entry: ${message}`;
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ export declare const getEntriesSchema: z.ZodObject<{
3
+ date_from: z.ZodOptional<z.ZodString>;
4
+ date_to: z.ZodOptional<z.ZodString>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ date_from?: string | undefined;
7
+ date_to?: string | undefined;
8
+ }, {
9
+ date_from?: string | undefined;
10
+ date_to?: string | undefined;
11
+ }>;
12
+ export type GetEntriesInput = z.infer<typeof getEntriesSchema>;
13
+ export declare function getEntries(input: GetEntriesInput): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { getHourEntries, getTags } from "../busy-api.js";
3
+ import { getConfiguredProjects, getUserId } from "../config.js";
4
+ export const getEntriesSchema = z.object({
5
+ date_from: z.string().optional().describe("Start date in YYYY-MM-DD format. Defaults to today"),
6
+ date_to: z.string().optional().describe("End date in YYYY-MM-DD format. Defaults to today"),
7
+ });
8
+ export async function getEntries(input) {
9
+ const userId = getUserId();
10
+ if (!userId) {
11
+ return "Not configured. Please run setup first.";
12
+ }
13
+ const entries = await getHourEntries(input.date_from, input.date_to, userId);
14
+ const projects = getConfiguredProjects();
15
+ let tags = [];
16
+ try {
17
+ tags = await getTags();
18
+ }
19
+ catch {
20
+ // Continue without tag names
21
+ }
22
+ if (entries.length === 0) {
23
+ const dateRange = input.date_from && input.date_to
24
+ ? `from ${input.date_from} to ${input.date_to}`
25
+ : "for today";
26
+ return `No time entries found ${dateRange}.`;
27
+ }
28
+ const formattedEntries = entries
29
+ .map((entry) => {
30
+ const project = projects.find((p) => p.id === entry.projectId);
31
+ const tag = tags.find((t) => t.id === entry.tagId);
32
+ const hours = entry.totalMinutes / 60;
33
+ const date = entry.startTime.split("T")[0];
34
+ return `- **${project?.name || `Project ${entry.projectId}`}** (${hours}h)\n Date: ${date}\n Tag: ${tag?.name || entry.tagId}\n Description: ${entry.description || "(none)"}\n ID: ${entry.id}`;
35
+ })
36
+ .join("\n\n");
37
+ const totalHours = entries.reduce((sum, e) => sum + e.totalMinutes, 0) / 60;
38
+ return `## Time Entries\n\n${formattedEntries}\n\n---\n**Total: ${totalHours} hours**`;
39
+ }
@@ -0,0 +1 @@
1
+ export declare function listProjects(): Promise<string>;
@@ -0,0 +1,23 @@
1
+ import { getConfiguredProjects } from "../config.js";
2
+ import { getTags } from "../busy-api.js";
3
+ export async function listProjects() {
4
+ const projects = getConfiguredProjects();
5
+ if (projects.length === 0) {
6
+ return "No projects configured. Please run setup first.";
7
+ }
8
+ const projectList = projects
9
+ .map((p) => {
10
+ const aliasText = p.aliases.length > 0 ? p.aliases.join(", ") : "none";
11
+ return `- **${p.name}** (ID: ${p.id})\n Aliases: ${aliasText}`;
12
+ })
13
+ .join("\n");
14
+ let tagList = "Unable to fetch tags.";
15
+ try {
16
+ const tags = await getTags();
17
+ tagList = tags.map((t) => `- ${t.name} (ID: ${t.id})`).join("\n");
18
+ }
19
+ catch {
20
+ // Keep default message
21
+ }
22
+ return `## Your Projects\n\n${projectList}\n\n## Available Tags\n\n${tagList}\n\n---\n**Note:** Recurring time entries cannot be modified via this integration.`;
23
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const reconfigureSchema: z.ZodObject<{
3
+ confirm: z.ZodBoolean;
4
+ }, "strip", z.ZodTypeAny, {
5
+ confirm: boolean;
6
+ }, {
7
+ confirm: boolean;
8
+ }>;
9
+ export type ReconfigureInput = z.infer<typeof reconfigureSchema>;
10
+ export declare function reconfigure(input: ReconfigureInput): Promise<string>;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { deleteConfig, getConfigPath } from "../setup/config-store.js";
3
+ export const reconfigureSchema = z.object({
4
+ confirm: z.boolean().describe("Set to true to confirm you want to reset configuration"),
5
+ });
6
+ export async function reconfigure(input) {
7
+ if (!input.confirm) {
8
+ return `This will delete your current configuration at \`${getConfigPath()}\` and restart setup.\n\nTo confirm, call reconfigure with confirm: true`;
9
+ }
10
+ deleteConfig();
11
+ return `Configuration deleted. The setup tools are now available again.\n\nUse setup_start to begin reconfiguration.`;
12
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ export declare const updateEntrySchema: z.ZodObject<{
3
+ entry_id: z.ZodString;
4
+ hours: z.ZodOptional<z.ZodNumber>;
5
+ description: z.ZodOptional<z.ZodString>;
6
+ project: z.ZodOptional<z.ZodString>;
7
+ tag: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ entry_id: string;
10
+ description?: string | undefined;
11
+ project?: string | undefined;
12
+ hours?: number | undefined;
13
+ tag?: string | undefined;
14
+ }, {
15
+ entry_id: string;
16
+ description?: string | undefined;
17
+ project?: string | undefined;
18
+ hours?: number | undefined;
19
+ tag?: string | undefined;
20
+ }>;
21
+ export type UpdateEntryInput = z.infer<typeof updateEntrySchema>;
22
+ export declare function updateEntry(input: UpdateEntryInput): Promise<string>;
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { updateHourEntry, getTags } from "../busy-api.js";
3
+ import { findProject, getConfiguredProjects } from "../config.js";
4
+ export const updateEntrySchema = z.object({
5
+ entry_id: z.string().describe("The ID of the entry to update"),
6
+ hours: z.number().positive().optional().describe("New number of hours"),
7
+ description: z.string().optional().describe("New description"),
8
+ project: z.string().optional().describe("New project name or alias"),
9
+ tag: z.string().optional().describe("New tag name"),
10
+ });
11
+ export async function updateEntry(input) {
12
+ const updates = {};
13
+ if (input.hours !== undefined) {
14
+ updates.totalMinutes = Math.round(input.hours * 60);
15
+ }
16
+ if (input.description !== undefined) {
17
+ updates.description = input.description;
18
+ }
19
+ if (input.project) {
20
+ const project = findProject(input.project);
21
+ if (!project) {
22
+ const projects = getConfiguredProjects();
23
+ const projectList = projects.map((p) => `- ${p.name}`).join("\n");
24
+ return `Could not find project matching "${input.project}". Available:\n${projectList}`;
25
+ }
26
+ updates.projectId = Number(project.id);
27
+ }
28
+ if (input.tag) {
29
+ try {
30
+ const tags = await getTags();
31
+ const tag = tags.find((t) => t.name.toLowerCase().includes(input.tag.toLowerCase()));
32
+ if (tag) {
33
+ updates.tagId = Number(tag.id);
34
+ }
35
+ else {
36
+ return `Could not find tag matching "${input.tag}".`;
37
+ }
38
+ }
39
+ catch {
40
+ return `Error fetching tags.`;
41
+ }
42
+ }
43
+ try {
44
+ const entry = await updateHourEntry(input.entry_id, updates);
45
+ return `Updated entry ${input.entry_id}:\n- totalMinutes: ${entry.totalMinutes}`;
46
+ }
47
+ catch (error) {
48
+ const message = error instanceof Error ? error.message : "Unknown error";
49
+ if (message.includes("recurring")) {
50
+ return `Cannot update entry ${input.entry_id}: This is a recurring entry and cannot be modified via the API. Please edit it in the Busy web app at https://app.busy.no`;
51
+ }
52
+ return `Error updating entry: ${message}`;
53
+ }
54
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "busy-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Busy time tracking - log hours with natural language via Claude",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "busy-mcp-server": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "dev": "tsc --watch",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "busy",
22
+ "time-tracking",
23
+ "claude",
24
+ "anthropic",
25
+ "model-context-protocol"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": ""
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "env-paths": "^3.0.0",
36
+ "zod": "^3.22.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.0.0",
40
+ "typescript": "^5.0.0"
41
+ },
42
+ "mcpName": "busy-time-tracker"
43
+ }