timezest-mcp 1.0.6 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,35 +1,37 @@
1
1
  # 📅 TimeZest MCP Server
2
2
 
3
- An MCP (Model Context Protocol) server that brings **TimeZest** scheduling data (confirmed appointments, pending invitations, and ticket-linked schedules) directly into your Claude conversations.
3
+ Bring your **TimeZest** scheduling data directly into Claude through the Model Context Protocol (MCP). Perform real-time scheduling lookups, engineer briefings, and ticket-linked appointment management without leaving your chat.
4
4
 
5
5
  ---
6
6
 
7
- ## 📋 Prerequisites (One-Time Setup)
7
+ ## 🌟 Features
8
8
 
9
- Before you begin, ensure you have **Node.js** installed on your machine. This is what allows Claude to run the server.
10
-
11
- 1. **Download Node.js**: [Download here (v18 or higher recommended)](https://nodejs.org/)
12
- 2. **Verify**: Open a terminal and type `node -v` to confirm it's installed.
9
+ - **Morning Briefings**: Instantly get confirmed vs. pending invitations for today, grouped by engineer.
10
+ - **Ticket Lookups**: Look up appointments by their original ConnectWise ticket number (e.g. #964400).
11
+ - **Engineer Schedules**: View upcoming timelines for specific staff members.
12
+ - **Pending Follow-ups**: Identify aging, unbooked invitations to prevent ticket stall.
13
+ - **Cancellation Insights**: Quickly triage cancelled requests for rebooking.
14
+ - **Enterprise Ready**: Full support for timezone transformations (TZ) and automated Vitest testing suite.
13
15
 
14
16
  ---
15
17
 
16
- ## 🚀 How to Install (For Colleagues)
18
+ ## 🚀 Quick Start (via npm/npx)
17
19
 
18
- You don't need to download or build any code. You can run this directly using `npx`.
20
+ The fastest way to use this server is via `npx`. No manual cloning or building is required to get started.
19
21
 
20
- ### Option A Claude Desktop (Standard)
22
+ ### 1. Configure Claude Desktop
21
23
  Open your Claude Desktop configuration file:
22
24
  - **Windows:** `%AppData%\Roaming\Claude\claude_desktop_config.json`
23
- - **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
25
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
24
26
 
25
- Add this entry to the `mcpServers` section:
27
+ Add the following entry to the `mcpServers` object:
26
28
 
27
29
  ```json
28
30
  {
29
31
  "mcpServers": {
30
32
  "timezest": {
31
- "command": "cmd",
32
- "args": ["/c", "npx", "-y", "timezest-mcp@latest"],
33
+ "command": "npx",
34
+ "args": ["-y", "timezest-mcp@latest"],
33
35
  "env": {
34
36
  "TIMEZEST_API_KEY": "YOUR_API_KEY_HERE",
35
37
  "TIMEZEST_DEFAULT_TZ": "America/Chicago"
@@ -39,50 +41,81 @@ Add this entry to the `mcpServers` section:
39
41
  }
40
42
  ```
41
43
 
42
- ### Option B Claude Code (CLI)
43
- If you use the terminal-based **Claude Code**, run this single command to add the server globally:
44
+ ### 2. Configure via Claude Code (CLI)
45
+ Run the following command in your terminal to add the server globally:
44
46
 
45
47
  ```bash
46
- claude mcp add-json timezest "{\"type\": \"stdio\", \"command\": \"cmd\", \"args\": [\"/c\", \"npx\", \"-y\", \"timezest-mcp@latest\"], \"env\": {\"TIMEZEST_API_KEY\": \"YOUR_API_KEY_HERE\", \"TIMEZEST_DEFAULT_TZ\": \"America/Chicago\"}}"
48
+ # Windows
49
+ claude mcp add-json timezest "{\"type\": \"stdio\", \"command\": \"cmd\", \"args\": [\"/c\", \"npx\", \"-y\", \"timezest-mcp@latest\"], \"env\": {\"TIMEZEST_API_KEY\": \"YOUR_API_KEY\", \"TIMEZEST_DEFAULT_TZ\": \"America/Chicago\"}}"
50
+
51
+ # macOS / Linux
52
+ claude mcp add-json timezest '{"type": "stdio", "command": "npx", "args": ["-y", "timezest-mcp@latest"], "env": {"TIMEZEST_API_KEY": "YOUR_API_KEY", "TIMEZEST_DEFAULT_TZ": "America/Chicago"}}'
47
53
  ```
48
54
 
49
55
  ---
50
56
 
51
57
  ## 💡 Example Prompts
58
+
52
59
  Once connected, try asking Claude:
53
60
  - *"What's the briefing for today?"*
54
- - *"Show me Aaron's schedule for next week"*
61
+ - *"Show me Scout's schedule for next week"*
55
62
  - *"Find any TimeZest requests for ticket #964400"*
56
- - *"Who has the oldest pending invitations?"*
57
- - *"Give me the appointment stats for the last 14 days"*
58
- - *"Show me recently cancelled appointments for Isam"*
63
+ - *"Provide appointment stats for the last 14 days"*
64
+ - *"Show me recently cancelled appointments for rebooking"*
59
65
 
60
66
  ---
61
67
 
62
68
  ## 🛠️ Available Tools
63
69
 
64
- | Tool | What It Does |
65
- |------|-------------|
70
+ | Tool | Purpose |
71
+ |------|---------|
66
72
  | `get_todays_appointments` | Morning briefing: Confirmed today vs. pending unbooked |
67
73
  | `list_appointments` | Flexible range search with engineer and status filters |
68
74
  | `get_engineer_schedule` | Sorted timeline for a specific engineer |
69
75
  | `list_pending_requests` | Follow up on `sent` and `new` (unbooked) invitations |
70
- | `find_appointment_by_ticket` | Instant lookup using ConnectWise ticket numbers |
71
- | `get_appointment_types` | List all available scheduling types (ID vs Name) |
72
- | `get_appointment_stats` | Management summary: Counts, minutes, and oldest pending |
73
- | `list_cancelled_appointments` | Triage cancelled requests for rebooking |
76
+ | `find_appointment_by_ticket` | Instant lookup using source ticket numbers |
77
+ | `get_appointment_stats` | Management summary: Counts, minutes, and aging requests |
78
+ | `list_cancelled_appointments` | Triage cancelled requests with rebooking URLs |
79
+
80
+ ---
81
+
82
+ ## 🔧 Development & Contributing
83
+
84
+ If you wish to contribute to the project or build the server from source, follow these steps:
85
+
86
+ ### Manual Installation
87
+ 1. **Clone the repository**:
88
+ ```bash
89
+ git clone https://github.com/sagarkalra-tech/TimeZest-MCP.git
90
+ ```
91
+ 2. **Install dependencies**:
92
+ ```bash
93
+ npm install
94
+ ```
95
+ 3. **Build the project**:
96
+ ```bash
97
+ npm run build
98
+ ```
99
+ 4. **Run the server locally**:
100
+ ```bash
101
+ npm start
102
+ ```
103
+
104
+ ### Running Tests
105
+ We use **Vitest** for all our transformations and API logic. To run the suite:
106
+ ```bash
107
+ npm run test
108
+ ```
74
109
 
75
110
  ---
76
111
 
77
- ## 📦 Technical Info
78
- - **Package Name:** `timezest-mcp`
79
- - **Platform:** Node.js (v18+)
80
- - **Environment Variables**:
81
- - `TIMEZEST_API_KEY` (Required): Your TimeZest Bearer token.
82
- - `TIMEZEST_DEFAULT_TZ` (Optional): Default IANA timezone (e.g. `America/Chicago`).
83
- - **Developer:** Sagar Kalra
112
+ ## ⚙️ Configuration Variables
113
+
114
+ - `TIMEZEST_API_KEY` (Required): Your TimeZest Bearer token.
115
+ - `TIMEZEST_DEFAULT_TZ` (Optional): Default IANA timezone (e.g. `America/Chicago`).
84
116
 
85
117
  ---
86
118
 
87
119
  ## 📄 License
120
+
88
121
  MIT © 2026 Sagar Kalra.
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { TimeZestClient } from '../client.js';
3
+ import axios from 'axios';
4
+ vi.mock('axios');
5
+ const mockedAxios = vi.mocked(axios);
6
+ describe('TimeZestClient', () => {
7
+ let client;
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ client = new TimeZestClient('test-api-key', { maxRetries: 1, retryDelayMs: 10 });
11
+ });
12
+ describe('getAppointmentTypes', () => {
13
+ it('fetches and caches appointment types', async () => {
14
+ mockedAxios.get.mockResolvedValueOnce({
15
+ data: [
16
+ { id: 'apty_1', internal_name: 'Remote Access' },
17
+ { id: 'apty_2', name: 'Phone Call' },
18
+ ],
19
+ });
20
+ const types = await client.getAppointmentTypes();
21
+ expect(types.get('apty_1')).toBe('Remote Access');
22
+ expect(types.get('apty_2')).toBe('Phone Call');
23
+ // Second call should use cache — no additional HTTP request
24
+ const types2 = await client.getAppointmentTypes();
25
+ expect(types2.size).toBe(2);
26
+ expect(mockedAxios.get).toHaveBeenCalledTimes(1);
27
+ });
28
+ it('prefers internal_name over name', async () => {
29
+ mockedAxios.get.mockResolvedValueOnce({
30
+ data: [{ id: 'apty_1', internal_name: 'Internal', name: 'External' }],
31
+ });
32
+ const types = await client.getAppointmentTypes();
33
+ expect(types.get('apty_1')).toBe('Internal');
34
+ });
35
+ it('throws a descriptive error on API failure', async () => {
36
+ mockedAxios.get.mockRejectedValueOnce({
37
+ response: { status: 401, data: { message: 'Unauthorized' } },
38
+ });
39
+ await expect(client.getAppointmentTypes()).rejects.toThrow(/401/);
40
+ });
41
+ });
42
+ describe('getSchedulingRequests', () => {
43
+ it('fetches scheduling requests with date window params', async () => {
44
+ mockedAxios.get.mockResolvedValueOnce({
45
+ data: { scheduling_requests: [{ id: 'sreq_1' }] },
46
+ });
47
+ const results = await client.getSchedulingRequests(7, 14);
48
+ expect(results).toHaveLength(1);
49
+ expect(results[0].id).toBe('sreq_1');
50
+ const callParams = mockedAxios.get.mock.calls[0][1]?.params;
51
+ expect(callParams).toHaveProperty('created_at_after');
52
+ expect(callParams).toHaveProperty('created_at_before');
53
+ expect(callParams.page).toBe(1);
54
+ });
55
+ });
56
+ describe('retry logic', () => {
57
+ it('retries on 429 rate limit', async () => {
58
+ mockedAxios.get
59
+ .mockRejectedValueOnce({ response: { status: 429, headers: {} } })
60
+ .mockResolvedValueOnce({ data: [{ id: 'apty_1', name: 'Test' }] });
61
+ const types = await client.getAppointmentTypes();
62
+ expect(types.size).toBe(1);
63
+ expect(mockedAxios.get).toHaveBeenCalledTimes(2);
64
+ });
65
+ it('retries on 500 server error', async () => {
66
+ mockedAxios.get
67
+ .mockRejectedValueOnce({ response: { status: 500 } })
68
+ .mockResolvedValueOnce({ data: [{ id: 'apty_1', name: 'Test' }] });
69
+ const types = await client.getAppointmentTypes();
70
+ expect(types.size).toBe(1);
71
+ expect(mockedAxios.get).toHaveBeenCalledTimes(2);
72
+ });
73
+ it('does not retry on 400 client error', async () => {
74
+ mockedAxios.get.mockRejectedValueOnce({
75
+ response: { status: 400, data: { message: 'Bad Request' } },
76
+ });
77
+ await expect(client.getAppointmentTypes()).rejects.toBeTruthy();
78
+ expect(mockedAxios.get).toHaveBeenCalledTimes(1);
79
+ });
80
+ it('exhausts retries and throws', async () => {
81
+ mockedAxios.get
82
+ .mockRejectedValueOnce({ response: { status: 500 } })
83
+ .mockRejectedValueOnce({ response: { status: 500, data: { message: 'Down' } } });
84
+ await expect(client.getAppointmentTypes()).rejects.toThrow(/500/);
85
+ expect(mockedAxios.get).toHaveBeenCalledTimes(2); // 1 initial + 1 retry
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { matchEngineer, filterByDateRange } from '../utils/filter.js';
3
+ function makeAppointment(overrides = {}) {
4
+ return {
5
+ id: 'sreq_1',
6
+ status: 'scheduled',
7
+ appointment_type: 'Remote Access to PC',
8
+ appointment_type_id: 'apty_1',
9
+ engineer: 'Scout Kalra',
10
+ engineer_id: 'agnt_1',
11
+ dispatched_from_team: false,
12
+ start_time: '2026-03-25T15:00:00.000Z',
13
+ start_time_local: '2026-03-25 10:00 AM GMT-5',
14
+ duration_mins: 30,
15
+ end_user_name: 'John Doe',
16
+ end_user_email: 'john@example.com',
17
+ ticket_number: '#12345',
18
+ ticket_type: 'service_ticket',
19
+ company_id: 100,
20
+ contact_id: 200,
21
+ scheduling_url: 'https://test.timezest.com/schedule/abc',
22
+ created_at: '2026-03-24T12:00:00.000Z',
23
+ updated_at: '2026-03-24T12:00:00.000Z',
24
+ age_hours: 24,
25
+ ...overrides,
26
+ };
27
+ }
28
+ describe('matchEngineer', () => {
29
+ it('matches by partial engineer name (case-insensitive)', () => {
30
+ expect(matchEngineer(makeAppointment(), 'scout')).toBe(true);
31
+ expect(matchEngineer(makeAppointment(), 'Scout')).toBe(true);
32
+ expect(matchEngineer(makeAppointment(), 'KALRA')).toBe(true);
33
+ });
34
+ it('matches by team name', () => {
35
+ const appt = makeAppointment({ dispatched_from_team: true, team_name: 'MSP Red Team' });
36
+ expect(matchEngineer(appt, 'red team')).toBe(true);
37
+ expect(matchEngineer(appt, 'MSP')).toBe(true);
38
+ });
39
+ it('returns false for non-matching query', () => {
40
+ expect(matchEngineer(makeAppointment(), 'Aaron')).toBe(false);
41
+ });
42
+ });
43
+ describe('filterByDateRange', () => {
44
+ const appointments = [
45
+ makeAppointment({ id: '1', start_time: '2026-03-24T10:00:00.000Z' }),
46
+ makeAppointment({ id: '2', start_time: '2026-03-25T10:00:00.000Z' }),
47
+ makeAppointment({ id: '3', start_time: '2026-03-26T10:00:00.000Z' }),
48
+ makeAppointment({ id: '4', start_time: '2026-03-27T10:00:00.000Z' }),
49
+ ];
50
+ it('filters appointments within date range', () => {
51
+ // end date parses as midnight, so only 3/25 10:00 AM fits within 3/25–3/26T00:00
52
+ const result = filterByDateRange(appointments, '2026-03-24', '2026-03-27');
53
+ expect(result.map(a => a.id)).toEqual(['1', '2', '3']);
54
+ });
55
+ it('returns empty array when no appointments match', () => {
56
+ const result = filterByDateRange(appointments, '2026-04-01', '2026-04-05');
57
+ expect(result).toEqual([]);
58
+ });
59
+ it('uses created_at for pending appointments with no start_time', () => {
60
+ const pending = [
61
+ makeAppointment({ id: 'p1', status: 'sent', start_time: null, created_at: '2026-03-25T10:00:00.000Z' }),
62
+ ];
63
+ const result = filterByDateRange(pending, '2026-03-25', '2026-03-26');
64
+ expect(result.map(a => a.id)).toEqual(['p1']);
65
+ });
66
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { transformAppointment } from '../utils/transform.js';
3
+ const appointmentTypes = new Map([
4
+ ['apty_123', 'Remote Access to PC'],
5
+ ['apty_456', 'Phone Call'],
6
+ ]);
7
+ function makeRawRequest(overrides = {}) {
8
+ return {
9
+ id: 'sreq_abc',
10
+ status: 'scheduled',
11
+ appointment_type_id: 'apty_123',
12
+ created_at: 1711382400, // 2024-03-25T16:00:00Z
13
+ updated_at: 1711382400,
14
+ selected_start_time: 1711468800, // 2024-03-26T16:00:00Z
15
+ duration_mins: 30,
16
+ end_user_name: 'John Doe',
17
+ end_user_email: 'john@example.com',
18
+ scheduling_url: 'https://test.timezest.com/schedule/abc',
19
+ scheduled_agents: [{ id: 'agnt_1', name: 'Scout Kalra' }],
20
+ resources: [{ id: 'agnt_1', name: 'Scout Kalra', object: 'agent' }],
21
+ associated_entities: [
22
+ { type: 'connectwise_psa/service_ticket', number: '#12345' },
23
+ { type: 'connectwise_psa/company', id: 100 },
24
+ { type: 'connectwise_psa/contact', id: 200 },
25
+ ],
26
+ ...overrides,
27
+ };
28
+ }
29
+ describe('transformAppointment', () => {
30
+ it('transforms a scheduled appointment with all fields', () => {
31
+ const result = transformAppointment(makeRawRequest(), appointmentTypes, 'America/Chicago');
32
+ expect(result.id).toBe('sreq_abc');
33
+ expect(result.status).toBe('scheduled');
34
+ expect(result.appointment_type).toBe('Remote Access to PC');
35
+ expect(result.engineer).toBe('Scout Kalra');
36
+ expect(result.engineer_id).toBe('agnt_1');
37
+ expect(result.duration_mins).toBe(30);
38
+ expect(result.end_user_name).toBe('John Doe');
39
+ expect(result.end_user_email).toBe('john@example.com');
40
+ expect(result.ticket_number).toBe('#12345');
41
+ expect(result.ticket_type).toBe('service_ticket');
42
+ expect(result.company_id).toBe(100);
43
+ expect(result.contact_id).toBe(200);
44
+ expect(result.start_time).toBeTruthy();
45
+ expect(result.start_time_local).toBeTruthy();
46
+ expect(result.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
47
+ expect(result.age_hours).toBeGreaterThan(0);
48
+ });
49
+ it('resolves engineer from scheduled_agents first', () => {
50
+ const result = transformAppointment(makeRawRequest({
51
+ scheduled_agents: [{ id: 'agnt_A', name: 'Agent A' }],
52
+ resources: [{ id: 'agnt_B', name: 'Agent B', object: 'agent' }],
53
+ }), appointmentTypes);
54
+ expect(result.engineer).toBe('Agent A');
55
+ expect(result.engineer_id).toBe('agnt_A');
56
+ });
57
+ it('falls back to resources when no scheduled_agents', () => {
58
+ const result = transformAppointment(makeRawRequest({
59
+ scheduled_agents: [],
60
+ resources: [{ id: 'agnt_B', name: 'Agent B', object: 'agent' }],
61
+ }), appointmentTypes);
62
+ expect(result.engineer).toBe('Agent B');
63
+ });
64
+ it('returns Unassigned when no agent available', () => {
65
+ const result = transformAppointment(makeRawRequest({
66
+ scheduled_agents: [],
67
+ resources: [{ id: 'team_1', name: 'MSP Team', object: 'team' }],
68
+ }), appointmentTypes);
69
+ expect(result.engineer).toBe('Unassigned');
70
+ expect(result.dispatched_from_team).toBe(true);
71
+ expect(result.team_name).toBe('MSP Team');
72
+ });
73
+ it('handles null start_time for pending requests', () => {
74
+ const result = transformAppointment(makeRawRequest({
75
+ status: 'sent',
76
+ selected_start_time: null,
77
+ }), appointmentTypes);
78
+ expect(result.start_time).toBeNull();
79
+ expect(result.start_time_local).toBeNull();
80
+ });
81
+ it('resolves project_ticket type', () => {
82
+ const result = transformAppointment(makeRawRequest({
83
+ associated_entities: [
84
+ { type: 'connectwise_psa/project_ticket', number: '#999' },
85
+ ],
86
+ }), appointmentTypes);
87
+ expect(result.ticket_type).toBe('project_ticket');
88
+ expect(result.ticket_number).toBe('#999');
89
+ });
90
+ it('handles unknown appointment type gracefully', () => {
91
+ const result = transformAppointment(makeRawRequest({
92
+ appointment_type_id: 'apty_unknown',
93
+ }), appointmentTypes);
94
+ expect(result.appointment_type).toBe('Unknown');
95
+ });
96
+ it('handles missing associated_entities', () => {
97
+ const result = transformAppointment(makeRawRequest({
98
+ associated_entities: [],
99
+ }), appointmentTypes);
100
+ expect(result.ticket_number).toBeNull();
101
+ expect(result.ticket_type).toBeNull();
102
+ expect(result.company_id).toBeNull();
103
+ expect(result.contact_id).toBeNull();
104
+ });
105
+ });
package/build/client.js CHANGED
@@ -4,17 +4,44 @@ export class TimeZestClient {
4
4
  apiKey;
5
5
  baseUrl = 'https://api.timezest.com/v1';
6
6
  appointmentTypes = new Map();
7
- constructor(apiKey) {
7
+ maxRetries;
8
+ retryDelayMs;
9
+ constructor(apiKey, { maxRetries = 3, retryDelayMs = 1000 } = {}) {
8
10
  this.apiKey = apiKey;
11
+ this.maxRetries = maxRetries;
12
+ this.retryDelayMs = retryDelayMs;
13
+ }
14
+ async fetchWithRetry(url, config) {
15
+ let lastError;
16
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
17
+ try {
18
+ return await axios.get(url, config);
19
+ }
20
+ catch (error) {
21
+ lastError = error;
22
+ const status = error?.response?.status;
23
+ // Only retry on network errors, 429, or 5xx
24
+ if (status && status !== 429 && status < 500)
25
+ throw error;
26
+ if (attempt < this.maxRetries) {
27
+ const delay = status === 429
28
+ ? (parseInt(error.response?.headers?.['retry-after'] || '0', 10) * 1000 || this.retryDelayMs * (attempt + 1))
29
+ : this.retryDelayMs * (attempt + 1);
30
+ await new Promise(r => setTimeout(r, delay));
31
+ }
32
+ }
33
+ }
34
+ throw lastError;
9
35
  }
10
36
  async fetchAllPages(endpoint, params = {}) {
11
37
  const results = [];
12
38
  let page = 1;
13
39
  let url = `${this.baseUrl}${endpoint}`;
14
40
  while (true) {
15
- const response = await axios.get(url, {
41
+ const response = await this.fetchWithRetry(url, {
16
42
  params: { ...params, page },
17
- headers: { Authorization: `Bearer ${this.apiKey}` }
43
+ headers: { Authorization: `Bearer ${this.apiKey}` },
44
+ timeout: 30000
18
45
  });
19
46
  const data = response.data;
20
47
  // Handle both flat arrays and wrapped responses (e.g., { scheduling_requests: [...] })
package/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "timezest-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "description": "MCP Server for TimeZest scheduling API — brings appointments, pending requests, and ticket-linked schedules to Claude.",
5
5
  "main": "build/index.js",
6
6
  "author": "Sagar Kalra",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sagarkalra-tech/TimeZest-MCP.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/sagarkalra-tech/TimeZest-MCP/issues"
14
+ },
15
+ "homepage": "https://github.com/sagarkalra-tech/TimeZest-MCP#readme",
7
16
  "keywords": [
8
17
  "mcp",
9
18
  "model-context-protocol",
@@ -20,22 +29,26 @@
20
29
  "build"
21
30
  ],
22
31
  "publishConfig": {
23
- "access": "public"
32
+ "access": "public",
33
+ "provenance": true
24
34
  },
25
35
  "scripts": {
26
36
  "build": "tsc",
27
37
  "watch": "tsc -w",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
28
40
  "prepublishOnly": "npm run build",
29
41
  "start": "node build/index.js"
30
42
  },
31
43
  "dependencies": {
32
- "@modelcontextprotocol/sdk": "^0.6.0",
44
+ "@modelcontextprotocol/sdk": "^1.28.0",
33
45
  "axios": "^1.6.0",
34
46
  "date-fns": "^3.0.0",
35
47
  "date-fns-tz": "^3.0.0"
36
48
  },
37
49
  "devDependencies": {
38
50
  "@types/node": "^20.0.0",
39
- "typescript": "^5.0.0"
51
+ "typescript": "^5.0.0",
52
+ "vitest": "^4.1.2"
40
53
  }
41
54
  }