@fluentdata-ai/tempo-mcp-server 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ivelin Ivanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/ivelin-web-tempo-mcp-server-badge.png)](https://mseep.ai/app/ivelin-web-tempo-mcp-server)
2
+
3
+ # Tempo MCP Server
4
+
5
+ A Model Context Protocol (MCP) server for managing Tempo worklogs in Jira. This server provides tools for tracking time and managing worklogs through Tempo's API, making it accessible through Claude, Cursor and other MCP-compatible clients.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@ivelin-web/tempo-mcp-server.svg)](https://www.npmjs.com/package/@ivelin-web/tempo-mcp-server)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ## Features
11
+
12
+ - **Retrieve Worklogs**: Get all worklogs for a specific date range
13
+ - **Create Worklog**: Log time against Jira issues
14
+ - **Bulk Create**: Create multiple worklogs in a single operation
15
+ - **Edit Worklog**: Modify time spent, dates, and descriptions
16
+ - **Delete Worklog**: Remove existing worklogs
17
+
18
+ ## System Requirements
19
+
20
+ - Node.js 18+ (LTS recommended)
21
+ - Jira Cloud instance
22
+ - Tempo API token
23
+ - Jira API token
24
+
25
+ ## Usage Options
26
+
27
+ There are two main ways to use this MCP server:
28
+
29
+ 1. **NPX (Recommended for most users)**: Run directly without installation
30
+ 2. **Local Clone**: Clone the repository for development or customization
31
+
32
+ ## Option 1: NPX Usage
33
+
34
+ The easiest way to use this server is via npx without installation:
35
+
36
+ ### Connecting to Claude Desktop (NPX Method)
37
+
38
+ 1. Open your MCP client configuration file:
39
+
40
+ - Claude Desktop (macOS): `~/Library/Application Support/Claude/claude_desktop_config.json`
41
+ - Claude Desktop (Windows): `%APPDATA%\Claude\claude_desktop_config.json`
42
+
43
+ 2. Add the following configuration:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "Jira_Tempo": {
49
+ "command": "npx",
50
+ "args": ["-y", "@esalgado/tempo-mcp-server"],
51
+ "env": {
52
+ "TEMPO_API_TOKEN": "your_tempo_api_token_here",
53
+ "JIRA_API_TOKEN": "your_jira_api_token_here",
54
+ "JIRA_EMAIL": "your_email@example.com",
55
+ "JIRA_BASE_URL": "https://your-org.atlassian.net"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ 3. Restart your Claude Desktop client
63
+
64
+ ### One-Click Install for Cursor
65
+
66
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=Jira%20Tempo&config=eyJjb21tYW5kIjoibnB4IC15IEBpdmVsaW4td2ViL3RlbXBvLW1jcC1zZXJ2ZXIiLCJlbnYiOnsiVEVNUE9fQVBJX1RPS0VOIjoieW91cl90ZW1wb19hcGlfdG9rZW5faGVyZSIsIkpJUkFfQVBJX1RPS0VOIjoieW91cl9qaXJhX2FwaV90b2tlbl9oZXJlIiwiSklSQV9FTUFJTCI6InlvdXJfZW1haWxAZXhhbXBsZS5jb20iLCJKSVJBX0JBU0VfVVJMIjoiaHR0cHM6Ly95b3VyLW9yZy5hdGxhc3NpYW4ubmV0In19)
67
+
68
+ ## Option 2: Local Repository Clone
69
+
70
+ ### Installation
71
+
72
+ ```bash
73
+ # Clone the repository
74
+ git clone https://github.com/ivelin-web/tempo-mcp-server.git
75
+ cd tempo-mcp-server
76
+
77
+ # Install dependencies
78
+ npm install
79
+
80
+ # Build TypeScript files
81
+ npm run build
82
+ ```
83
+
84
+ ### Running Locally
85
+
86
+ There are two ways to run the server locally:
87
+
88
+ #### 1. Using the MCP Inspector (for development and debugging)
89
+
90
+ ```bash
91
+ npm run inspect
92
+ ```
93
+
94
+ #### 2. Using Node directly
95
+
96
+ You can run the server directly with Node by pointing to the built JavaScript file:
97
+
98
+ ### Connecting to Claude Desktop (Local Method)
99
+
100
+ 1. Open your MCP client configuration file
101
+ 2. Add the following configuration:
102
+
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "Jira_Tempo": {
107
+ "command": "node",
108
+ "args": ["/ABSOLUTE/PATH/TO/tempo-mcp-server/build/index.js"],
109
+ "env": {
110
+ "TEMPO_API_TOKEN": "your_tempo_api_token_here",
111
+ "JIRA_API_TOKEN": "your_jira_api_token_here",
112
+ "JIRA_EMAIL": "your_email@example.com",
113
+ "JIRA_BASE_URL": "https://your-org.atlassian.net"
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ 3. Restart your Claude Desktop client
121
+
122
+ ## Getting API Tokens
123
+
124
+ 1. **Tempo API Token**:
125
+
126
+ - Go to Tempo > Settings > API Integration
127
+ - Create a new API token with appropriate permissions
128
+
129
+ 2. **Jira API Token**:
130
+ - Go to [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
131
+ - Create a new API token for your account
132
+
133
+ ## Environment Variables
134
+
135
+ The server requires the following environment variables:
136
+
137
+ ```
138
+ TEMPO_API_TOKEN # Your Tempo API token
139
+ JIRA_API_TOKEN # Your Jira API token
140
+ JIRA_EMAIL # Your Jira account email (required for basic auth)
141
+ JIRA_BASE_URL # Your Jira instance URL (e.g., https://your-org.atlassian.net)
142
+ JIRA_AUTH_TYPE # Optional: 'basic' (default) or 'bearer' for OAuth 2.0 tokens
143
+ JIRA_TEMPO_ACCOUNT_CUSTOM_FIELD_ID # Optional: Custom field ID for Tempo accounts
144
+ ```
145
+
146
+ You can set these in your environment or provide them in the MCP client configuration.
147
+
148
+ ### Authentication Types
149
+
150
+ The server supports two authentication methods for the Jira API:
151
+
152
+ #### Basic Authentication (default)
153
+
154
+ Uses email and API token. This is the traditional method:
155
+
156
+ ```json
157
+ {
158
+ "env": {
159
+ "JIRA_API_TOKEN": "your_api_token",
160
+ "JIRA_EMAIL": "your_email@example.com",
161
+ "JIRA_AUTH_TYPE": "basic"
162
+ }
163
+ }
164
+ ```
165
+
166
+ #### Bearer Token Authentication (OAuth 2.0)
167
+
168
+ For users who want to use OAuth 2.0 scoped tokens for enhanced security:
169
+
170
+ ```json
171
+ {
172
+ "env": {
173
+ "JIRA_API_TOKEN": "your_oauth_access_token",
174
+ "JIRA_AUTH_TYPE": "bearer"
175
+ }
176
+ }
177
+ ```
178
+
179
+ Note: When using `bearer` auth, `JIRA_EMAIL` is not required as the user is identified from the token.
180
+
181
+ ## Tempo Account Configuration
182
+
183
+ If your Tempo instance requires worklogs to be linked to accounts, set the custom field ID that contains the account information:
184
+
185
+ ```bash
186
+ JIRA_TEMPO_ACCOUNT_CUSTOM_FIELD_ID=10234
187
+ ```
188
+
189
+ To find your custom field ID:
190
+
191
+ 1. Go to Jira Settings → Issues → Custom Fields
192
+ 2. Find your Tempo account field and note the ID from the URL or field configuration
193
+
194
+ ## Available Tools
195
+
196
+ ### retrieveWorklogs
197
+
198
+ Fetches worklogs for the configured user between start and end dates.
199
+
200
+ ```
201
+ Parameters:
202
+ - startDate: String (YYYY-MM-DD)
203
+ - endDate: String (YYYY-MM-DD)
204
+ ```
205
+
206
+ ### createWorklog
207
+
208
+ Creates a new worklog for a specific Jira issue.
209
+
210
+ ```
211
+ Parameters:
212
+ - issueKey: String (e.g., "PROJECT-123")
213
+ - timeSpentHours: Number (positive)
214
+ - date: String (YYYY-MM-DD)
215
+ - description: String (optional)
216
+ - startTime: String (HH:MM format, optional)
217
+ ```
218
+
219
+ ### bulkCreateWorklogs
220
+
221
+ Creates multiple worklogs in a single operation.
222
+
223
+ ```
224
+ Parameters:
225
+ - worklogEntries: Array of {
226
+ issueKey: String
227
+ timeSpentHours: Number
228
+ date: String (YYYY-MM-DD)
229
+ description: String (optional)
230
+ startTime: String (HH:MM format, optional)
231
+ }
232
+ ```
233
+
234
+ ### editWorklog
235
+
236
+ Modifies an existing worklog.
237
+
238
+ ```
239
+ Parameters:
240
+ - worklogId: String
241
+ - timeSpentHours: Number (positive)
242
+ - description: String (optional)
243
+ - date: String (YYYY-MM-DD, optional)
244
+ - startTime: String (HH:MM format, optional)
245
+ ```
246
+
247
+ ### deleteWorklog
248
+
249
+ Removes an existing worklog.
250
+
251
+ ```
252
+ Parameters:
253
+ - worklogId: String
254
+ ```
255
+
256
+ ## Project Structure
257
+
258
+ ```
259
+ tempo-mcp-server/
260
+ ├── src/ # Source code
261
+ │ ├── config.ts # Configuration management
262
+ │ ├── index.ts # MCP server implementation
263
+ │ ├── jira.ts # Jira API integration
264
+ │ ├── tools.ts # Tool implementations
265
+ │ ├── types.ts # TypeScript types and schemas
266
+ │ └── utils.ts # Utility functions
267
+ ├── build/ # Compiled JavaScript (generated)
268
+ ├── tsconfig.json # TypeScript configuration
269
+ └── package.json # Project metadata and scripts
270
+ ```
271
+
272
+ ## Troubleshooting
273
+
274
+ If you encounter issues:
275
+
276
+ 1. Check that all environment variables are properly set
277
+ 2. Verify your Jira and Tempo API tokens have the correct permissions
278
+ 3. Check the console output for error messages
279
+ 4. Try running with the inspector: `npm run inspect`
280
+
281
+ ## License
282
+
283
+ [MIT](LICENSE)
284
+
285
+ ## Credits
286
+
287
+ This server implements the [Model Context Protocol](https://modelcontextprotocol.io/) specification created by Anthropic.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Configuration manager for the MCP server
3
+ * Validates required environment variables and exports config settings
4
+ */
5
+ import { envSchema } from './types.js';
6
+ import { ZodError } from 'zod';
7
+ import { config as loadEnv } from 'dotenv';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ // Load environment variables from .env file
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const envLoadResult = loadEnv({ path: path.resolve(__dirname, '../.env') });
14
+ if (envLoadResult.error) {
15
+ console.error('[WARN] Could not load .env file:', envLoadResult.error.message);
16
+ }
17
+ // Validate environment variables
18
+ function validateEnv() {
19
+ try {
20
+ // Parse and validate environment variables
21
+ return envSchema.parse(process.env);
22
+ }
23
+ catch (error) {
24
+ // Format and display validation errors
25
+ console.error('[ERROR] Environment validation failed:');
26
+ if (error instanceof ZodError) {
27
+ error.issues.forEach((err) => {
28
+ console.error(`- ${err.path.join('.')}: ${err.message}`);
29
+ });
30
+ }
31
+ else {
32
+ console.error(error instanceof Error ? error.message : String(error));
33
+ }
34
+ process.exit(1);
35
+ }
36
+ }
37
+ // Get validated environment variables
38
+ const env = validateEnv();
39
+ // Application configuration with validated environment variables
40
+ const config = {
41
+ tempoApi: {
42
+ baseUrl: 'https://api.tempo.io/4',
43
+ token: env.TEMPO_API_TOKEN,
44
+ },
45
+ jiraApi: {
46
+ baseUrl: env.JIRA_BASE_URL,
47
+ token: env.JIRA_API_TOKEN,
48
+ email: env.JIRA_EMAIL,
49
+ authType: env.JIRA_AUTH_TYPE,
50
+ tempoAccountCustomFieldId: env.JIRA_TEMPO_ACCOUNT_CUSTOM_FIELD_ID || undefined,
51
+ },
52
+ server: {
53
+ name: 'tempo-mcp-server',
54
+ version: '1.0.0',
55
+ },
56
+ };
57
+ export default config;
package/build/index.js ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tempo MCP Server
4
+ *
5
+ * A simple Model Context Protocol server for managing Tempo worklogs with TypeScript.
6
+ */
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import config from './config.js';
10
+ import * as tools from './tools.js';
11
+ import { retrieveWorklogsSchema, createWorklogSchema, } from './types.js';
12
+ // Create MCP server instance
13
+ const server = new McpServer({
14
+ name: config.server.name,
15
+ version: config.server.version,
16
+ }, {
17
+ capabilities: { logging: {} },
18
+ });
19
+ // Tool: retrieveWorklogs - fetch worklogs between two dates
20
+ server.registerTool('retrieveWorklogs', {
21
+ title: 'Retrieve Worklogs',
22
+ description: 'Retrieve worklogs for a specified date range.',
23
+ inputSchema: retrieveWorklogsSchema.shape,
24
+ }, async ({ startDate, endDate }) => {
25
+ try {
26
+ const result = await tools.retrieveWorklogs(startDate, endDate);
27
+ return {
28
+ content: result.content,
29
+ };
30
+ }
31
+ catch (error) {
32
+ console.error(`[ERROR] retrieveWorklogs failed: ${error instanceof Error ? error.message : String(error)}`);
33
+ return {
34
+ content: [
35
+ {
36
+ type: 'text',
37
+ text: `Error retrieving worklogs: ${error instanceof Error ? error.message : String(error)}`,
38
+ },
39
+ ],
40
+ isError: true,
41
+ };
42
+ }
43
+ });
44
+ // Tool: createWorklog - create a single worklog entry
45
+ server.registerTool('createWorklog', {
46
+ title: 'Create Worklog',
47
+ description: 'Create a new worklog entry.',
48
+ inputSchema: createWorklogSchema.shape,
49
+ }, async ({ issueKey, timeSpentHours, date, description, startTime, remainingEstimateHours, }) => {
50
+ try {
51
+ const result = await tools.createWorklog(issueKey, timeSpentHours, date, description, startTime, remainingEstimateHours);
52
+ return {
53
+ content: result.content,
54
+ };
55
+ }
56
+ catch (error) {
57
+ console.error(`[ERROR] createWorklog failed: ${error instanceof Error ? error.message : String(error)}`);
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text',
62
+ text: `Error creating worklog: ${error instanceof Error ? error.message : String(error)}`,
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
68
+ });
69
+ // // Tool: bulkCreateWorklogs - create multiple worklog entries at once
70
+ // server.registerTool(
71
+ // 'bulkCreateWorklogs',
72
+ // bulkCreateWorklogsSchema.shape,
73
+ // async ({ worklogEntries }) => {
74
+ // try {
75
+ // const result = await tools.bulkCreateWorklogs(worklogEntries);
76
+ // return {
77
+ // content: result.content,
78
+ // };
79
+ // } catch (error) {
80
+ // console.error(
81
+ // `[ERROR] bulkCreateWorklogs failed: ${error instanceof Error ? error.message : String(error)}`,
82
+ // );
83
+ // return {
84
+ // content: [
85
+ // {
86
+ // type: 'text',
87
+ // text: `Error creating multiple worklogs: ${error instanceof Error ? error.message : String(error)}`,
88
+ // },
89
+ // ],
90
+ // isError: true,
91
+ // };
92
+ // }
93
+ // },
94
+ // );
95
+ //
96
+ // // Tool: editWorklog - modify an existing worklog entry
97
+ // server.registerTool(
98
+ // 'editWorklog',
99
+ // editWorklogSchema.shape,
100
+ // async ({ worklogId, timeSpentHours, description, date, startTime }) => {
101
+ // try {
102
+ // const result = await tools.editWorklog(
103
+ // worklogId,
104
+ // timeSpentHours,
105
+ // description || null,
106
+ // date || null,
107
+ // startTime || undefined,
108
+ // );
109
+ // return {
110
+ // content: result.content,
111
+ // };
112
+ // } catch (error) {
113
+ // console.error(
114
+ // `[ERROR] editWorklog failed: ${error instanceof Error ? error.message : String(error)}`,
115
+ // );
116
+ // return {
117
+ // content: [
118
+ // {
119
+ // type: 'text',
120
+ // text: `Error editing worklog: ${error instanceof Error ? error.message : String(error)}`,
121
+ // },
122
+ // ],
123
+ // isError: true,
124
+ // };
125
+ // }
126
+ // },
127
+ // );
128
+ //
129
+ // // Tool: deleteWorklog - remove an existing worklog entry
130
+ // server.registerTool(
131
+ // 'deleteWorklog',
132
+ // deleteWorklogSchema.shape,
133
+ // async ({ worklogId }) => {
134
+ // try {
135
+ // const result = await tools.deleteWorklog(worklogId);
136
+ // return {
137
+ // content: result.content,
138
+ // };
139
+ // } catch (error) {
140
+ // console.error(
141
+ // `[ERROR] deleteWorklog failed: ${error instanceof Error ? error.message : String(error)}`,
142
+ // );
143
+ // return {
144
+ // content: [
145
+ // {
146
+ // type: 'text',
147
+ // text: `Error deleting worklog: ${error instanceof Error ? error.message : String(error)}`,
148
+ // },
149
+ // ],
150
+ // isError: true,
151
+ // };
152
+ // }
153
+ // },
154
+ // );
155
+ //
156
+ async function startServer() {
157
+ try {
158
+ const transport = new StdioServerTransport();
159
+ await server.connect(transport);
160
+ console.error('[INFO] MCP Server started successfully');
161
+ }
162
+ catch (error) {
163
+ console.error(`[ERROR] Failed to start MCP Server: ${error instanceof Error ? error.message : String(error)}`);
164
+ if (error instanceof Error && error.stack) {
165
+ console.error(`[ERROR] Stack trace: ${error.stack}`);
166
+ }
167
+ process.exit(1);
168
+ }
169
+ }
170
+ startServer().catch((error) => {
171
+ console.error(`[ERROR] Unhandled exception: ${error instanceof Error ? error.message : String(error)}`);
172
+ process.exit(1);
173
+ });
174
+ export default server;
package/build/jira.js ADDED
@@ -0,0 +1,105 @@
1
+ import axios from 'axios';
2
+ import { issueIdSchema, idOrKeySchema, } from './types.js';
3
+ import config from './config.js';
4
+ // Build authorization header based on auth type
5
+ function getAuthHeader() {
6
+ if (config.jiraApi.authType === 'bearer') {
7
+ return `Bearer ${config.jiraApi.token}`;
8
+ }
9
+ // Basic auth (default)
10
+ return `Basic ${Buffer.from(`${config.jiraApi.email}:${config.jiraApi.token}`).toString('base64')}`;
11
+ }
12
+ // Jira API client with authentication
13
+ const jiraApi = axios.create({
14
+ baseURL: `${config.jiraApi.baseUrl}/rest/api/3`,
15
+ headers: {
16
+ Authorization: getAuthHeader(),
17
+ Accept: 'application/json',
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ });
21
+ // Standardized error handling for Jira API
22
+ function formatJiraError(error, context) {
23
+ if (axios.isAxiosError(error)) {
24
+ const statusCode = error.response?.status;
25
+ const message = error.response?.data?.message ||
26
+ error.response?.data?.errorMessages?.join(', ') ||
27
+ error.message;
28
+ return new Error(`${context}: ${statusCode} - ${message}`);
29
+ }
30
+ return new Error(`${context}: ${error.message}`);
31
+ }
32
+ /**
33
+ * Get user's account ID.
34
+ * - For Bearer auth: uses /myself endpoint
35
+ * - For Basic auth: searches by configured email
36
+ */
37
+ export async function getCurrentUserAccountId() {
38
+ if (config.jiraApi.authType === 'bearer') {
39
+ // Bearer auth: get current user directly
40
+ const response = await jiraApi.get('/myself');
41
+ return response.data.accountId;
42
+ }
43
+ // Basic auth: search by email
44
+ const response = await jiraApi
45
+ .get('user/search', {
46
+ params: { query: config.jiraApi.email },
47
+ })
48
+ .catch((error) => {
49
+ throw new Error(`could not fetch user from jira: ${error}`);
50
+ });
51
+ const users = response.data;
52
+ if (!users || users.length === 0) {
53
+ throw new Error(`No user found with email: ${config.jiraApi.email}`);
54
+ }
55
+ // Find exact email match
56
+ const user = users.find((u) => u.emailAddress === config.jiraApi.email);
57
+ if (!user) {
58
+ throw new Error(`No exact match for email: ${config.jiraApi.email}`);
59
+ }
60
+ return user.accountId;
61
+ }
62
+ /**
63
+ * Get Jira issue key from issue ID
64
+ */
65
+ export async function getIssueKeyById(issueId) {
66
+ try {
67
+ // Validate issue ID using the schema
68
+ const result = issueIdSchema().safeParse(issueId);
69
+ if (!result.success) {
70
+ throw new Error(result.error.issues[0].message || 'Issue ID validation failed');
71
+ }
72
+ const response = await jiraApi.get(`/issue/${issueId}`);
73
+ return response.data.key;
74
+ }
75
+ catch (error) {
76
+ throw formatJiraError(error, `Failed to get issue key for ID ${issueId}`);
77
+ }
78
+ }
79
+ /**
80
+ * Get Jira issue from issue ID or key
81
+ */
82
+ export async function getIssue(idOrKey) {
83
+ try {
84
+ // Validate issue ID using the schema
85
+ const result = idOrKeySchema().safeParse(idOrKey);
86
+ if (!result.success) {
87
+ throw new Error(result.error.issues[0].message || 'Issue identifier validation failed');
88
+ }
89
+ const response = await jiraApi.get(`/issue/${idOrKey}`);
90
+ // Find the Tempo account key
91
+ const tempoAccountId = config.jiraApi.tempoAccountCustomFieldId
92
+ ? response.data.fields[`customfield_${config.jiraApi.tempoAccountCustomFieldId}`]?.id
93
+ : undefined;
94
+ const id = response.data.id;
95
+ const key = response.data.key;
96
+ return {
97
+ id,
98
+ key,
99
+ ...(tempoAccountId ? { tempoAccountId } : {}),
100
+ };
101
+ }
102
+ catch (error) {
103
+ throw formatJiraError(error, `Failed to get issue for ${idOrKey}`);
104
+ }
105
+ }
package/build/tools.js ADDED
@@ -0,0 +1,390 @@
1
+ import axios from 'axios';
2
+ import config from './config.js';
3
+ import { getCurrentUserAccountId, getIssue } from './jira.js';
4
+ import { formatError, getIssueKeysMap, calculateEndTime } from './utils.js';
5
+ // API client for Tempo
6
+ const api = axios.create({
7
+ baseURL: config.tempoApi.baseUrl,
8
+ headers: {
9
+ Authorization: `Bearer ${config.tempoApi.token}`,
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ });
13
+ /**
14
+ * Retrieve worklogs for the configured user within a date range
15
+ */
16
+ export async function retrieveWorklogs(startDate, endDate) {
17
+ try {
18
+ const accountId = await getCurrentUserAccountId();
19
+ // Fetch all pages of worklogs
20
+ let allWorklogs = [];
21
+ let nextUrl = null;
22
+ let isFirstRequest = true;
23
+ let pageCount = 0;
24
+ const MAX_PAGES = 500; // Safety limit to prevent infinite loops (25,000 worklogs max)
25
+ do {
26
+ if (pageCount >= MAX_PAGES) {
27
+ console.warn(`Reached maximum page limit (${MAX_PAGES}) while fetching worklogs`);
28
+ break;
29
+ }
30
+ let response;
31
+ if (isFirstRequest) {
32
+ response = await api
33
+ .get(`/worklogs/user/${accountId}`, {
34
+ params: { from: startDate, to: endDate },
35
+ })
36
+ .catch((error) => {
37
+ if (error.response?.status === 401) {
38
+ throw new Error('Invalid API token. Please check your Tempo API token and try again.');
39
+ }
40
+ throw error;
41
+ });
42
+ isFirstRequest = false;
43
+ }
44
+ else {
45
+ const expectedOrigin = new URL(config.tempoApi.baseUrl).origin;
46
+ const nextUrlOrigin = new URL(nextUrl).origin;
47
+ if (nextUrlOrigin !== expectedOrigin) {
48
+ throw new Error(`Invalid pagination URL: expected origin ${expectedOrigin}, got ${nextUrlOrigin}`);
49
+ }
50
+ response = await axios.get(nextUrl, {
51
+ headers: {
52
+ Authorization: `Bearer ${config.tempoApi.token}`,
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ });
56
+ }
57
+ const pageWorklogs = response.data.results || [];
58
+ allWorklogs = allWorklogs.concat(pageWorklogs);
59
+ // Check if there's a next page
60
+ nextUrl = response?.data?.metadata?.next || null;
61
+ pageCount++;
62
+ } while (nextUrl);
63
+ const worklogs = allWorklogs;
64
+ // If no worklogs found, return empty content
65
+ if (worklogs.length === 0) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: 'No worklogs found for the specified date range.',
71
+ },
72
+ ],
73
+ };
74
+ }
75
+ // Get issue keys for all worklogs
76
+ const issueIdToKeyMap = await getIssueKeysMap(worklogs);
77
+ // Format the response
78
+ const formattedContent = worklogs.map((worklog) => {
79
+ const tempoWorklogId = worklog.tempoWorklogId || 'Unknown';
80
+ const issueId = worklog.issue?.id || 'Unknown';
81
+ const issueKey = issueIdToKeyMap[issueId] || 'Unknown';
82
+ const description = worklog.description || 'No description';
83
+ const timeSpentHours = (worklog.timeSpentSeconds / 3600).toFixed(2);
84
+ const date = worklog.startDate || 'Unknown';
85
+ const startTime = worklog.startTime || '';
86
+ return {
87
+ type: 'text',
88
+ text: `TempoWorklogId: ${tempoWorklogId} | IssueKey: ${issueKey} | IssueId: ${issueId} | Date: ${date}${startTime ? ` | StartTime: ${startTime}` : ''} | Hours: ${timeSpentHours} | Description: ${description}`,
89
+ };
90
+ });
91
+ return {
92
+ content: formattedContent,
93
+ metadata: {
94
+ totalCount: worklogs.length,
95
+ pagesProcessed: pageCount,
96
+ startDate,
97
+ endDate,
98
+ },
99
+ };
100
+ }
101
+ catch (error) {
102
+ return {
103
+ isError: true,
104
+ content: [
105
+ {
106
+ type: 'text',
107
+ text: `Error retrieving worklogs: ${formatError(error)}`,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ }
113
+ /**
114
+ * Create a new worklog
115
+ */
116
+ export async function createWorklog(issueKey, timeSpentHours, date, description, startTime = undefined, remainingEstimateHours = undefined) {
117
+ try {
118
+ // Get issue ID and account ID
119
+ const issue = await getIssue(issueKey);
120
+ const accountId = await getCurrentUserAccountId();
121
+ const { id: issueId } = issue;
122
+ const account = await fetchTempoAccountFromIssue(issue);
123
+ const timeSpentSeconds = Math.round(timeSpentHours * 3600);
124
+ // Prepare payload
125
+ const payload = {
126
+ issueId: Number(issueId),
127
+ timeSpentSeconds,
128
+ billableSeconds: timeSpentSeconds,
129
+ startDate: date,
130
+ authorAccountId: accountId,
131
+ description: description || null,
132
+ ...(startTime && { startTime: `${startTime}:00` }),
133
+ ...(remainingEstimateHours !== undefined && {
134
+ remainingEstimateSeconds: Math.round(remainingEstimateHours * 3600),
135
+ }),
136
+ ...(account && {
137
+ attributes: [{ key: '_Account_', value: account.key }],
138
+ }),
139
+ };
140
+ // Submit the worklog
141
+ const response = await api.post('/worklogs', payload);
142
+ // Calculate end time if start time is provided
143
+ let timeInfo = '';
144
+ if (startTime) {
145
+ const endTime = calculateEndTime(startTime, timeSpentHours);
146
+ timeInfo = ` starting at ${startTime} and ending at ${endTime}`;
147
+ }
148
+ const accountInfo = account ? ` with account '${account.name}'` : '';
149
+ return {
150
+ content: [
151
+ {
152
+ type: 'text',
153
+ text: `Worklog with ID ${response.data.tempoWorklogId} created successfully for ${issueKey}${accountInfo}. Time logged: ${timeSpentHours} hours on ${date}${timeInfo}`,
154
+ },
155
+ ],
156
+ };
157
+ }
158
+ catch (error) {
159
+ console.error(error);
160
+ return {
161
+ isError: true,
162
+ content: [
163
+ {
164
+ type: 'text',
165
+ text: `Failed to create worklog: ${formatError(error)}`,
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ }
171
+ /**
172
+ * Create multiple worklogs
173
+ */
174
+ export async function bulkCreateWorklogs(worklogEntries) {
175
+ try {
176
+ // Get user account ID
177
+ const authorAccountId = await getCurrentUserAccountId();
178
+ // Group entries by issue key
179
+ const entriesByIssueKey = {};
180
+ worklogEntries.forEach((entry) => {
181
+ if (!entriesByIssueKey[entry.issueKey]) {
182
+ entriesByIssueKey[entry.issueKey] = [];
183
+ }
184
+ entriesByIssueKey[entry.issueKey].push(entry);
185
+ });
186
+ const results = [];
187
+ const errors = [];
188
+ // Process each issue's entries
189
+ for (const [issueKey, entries] of Object.entries(entriesByIssueKey)) {
190
+ try {
191
+ const issue = await getIssue(issueKey);
192
+ const account = await fetchTempoAccountFromIssue(issue);
193
+ // Format entries for API
194
+ const formattedEntries = entries.map((entry) => ({
195
+ timeSpentSeconds: Math.round(entry.timeSpentHours * 3600),
196
+ startDate: entry.date,
197
+ authorAccountId,
198
+ description: entry.description || '',
199
+ ...(entry.startTime && { startTime: `${entry.startTime}:00` }),
200
+ ...(account && {
201
+ attributes: [{ key: '_Account_', value: account.key }],
202
+ }),
203
+ }));
204
+ const { id: issueId } = issue;
205
+ // Submit bulk request
206
+ const response = await api.post(`/worklogs/issue/${Number(issueId)}/bulk`, formattedEntries);
207
+ const createdWorklogs = response.data || [];
208
+ // Record results
209
+ entries.forEach((entry, i) => {
210
+ const created = createdWorklogs[i] || null;
211
+ // Calculate end time if startTime is provided
212
+ let endTime = undefined;
213
+ if (entry.startTime && created) {
214
+ endTime = calculateEndTime(entry.startTime, entry.timeSpentHours);
215
+ }
216
+ results.push({
217
+ issueKey,
218
+ timeSpentHours: entry.timeSpentHours,
219
+ date: entry.date,
220
+ worklogId: created?.tempoWorklogId || null,
221
+ success: !!created,
222
+ startTime: entry.startTime,
223
+ endTime,
224
+ account: account?.name,
225
+ });
226
+ });
227
+ }
228
+ catch (error) {
229
+ const errorMessage = formatError(error);
230
+ // Record errors
231
+ entries.forEach((entry) => {
232
+ errors.push({
233
+ issueKey,
234
+ timeSpentHours: entry.timeSpentHours,
235
+ date: entry.date,
236
+ error: errorMessage,
237
+ });
238
+ });
239
+ }
240
+ }
241
+ // Create content for response
242
+ const content = [];
243
+ const successCount = results.filter((r) => r.success).length;
244
+ // Add success messages
245
+ if (successCount > 0) {
246
+ content.push({
247
+ type: 'text',
248
+ text: `Successfully created ${successCount} worklogs:`,
249
+ });
250
+ results
251
+ .filter((r) => r.success)
252
+ .forEach((result) => {
253
+ let timeInfo = '';
254
+ if (result.startTime) {
255
+ timeInfo = ` starting at ${result.startTime}${result.endTime ? ` and ending at ${result.endTime}` : ''}`;
256
+ }
257
+ const accountInfo = result.account
258
+ ? ` for account '${result.account}'`
259
+ : '';
260
+ content.push({
261
+ type: 'text',
262
+ text: `- Issue ${result.issueKey}: ${result.timeSpentHours} hours on ${result.date}${timeInfo}${accountInfo}`,
263
+ });
264
+ });
265
+ }
266
+ // Add error messages
267
+ if (errors.length > 0) {
268
+ content.push({
269
+ type: 'text',
270
+ text: `Failed to create ${errors.length} worklogs:`,
271
+ });
272
+ errors.forEach((error) => {
273
+ content.push({
274
+ type: 'text',
275
+ text: `- Issue ${error.issueKey}: ${error.timeSpentHours} hours on ${error.date}. Error: ${error.error}`,
276
+ });
277
+ });
278
+ }
279
+ return {
280
+ content,
281
+ metadata: {
282
+ totalSuccess: successCount,
283
+ totalFailure: errors.length,
284
+ details: {
285
+ successes: results.filter((r) => r.success),
286
+ failures: errors,
287
+ },
288
+ },
289
+ isError: errors.length > 0 && successCount === 0,
290
+ };
291
+ }
292
+ catch (error) {
293
+ return {
294
+ isError: true,
295
+ content: [
296
+ {
297
+ type: 'text',
298
+ text: `Error processing bulk worklogs: ${formatError(error)}`,
299
+ },
300
+ ],
301
+ };
302
+ }
303
+ }
304
+ /**
305
+ * Edit an existing worklog
306
+ */
307
+ export async function editWorklog(worklogId, timeSpentHours, description = null, date = null, startTime = undefined) {
308
+ try {
309
+ // Get current worklog
310
+ const response = await api.get(`/worklogs/${worklogId}`);
311
+ const worklog = response.data;
312
+ // Prepare update payload
313
+ const updatePayload = {
314
+ authorAccountId: worklog.author.accountId,
315
+ startDate: date || worklog.startDate,
316
+ timeSpentSeconds: Math.round(timeSpentHours * 3600),
317
+ billableSeconds: Math.round(timeSpentHours * 3600),
318
+ ...(description !== null && { description }),
319
+ ...(startTime && { startTime: `${startTime}:00` }),
320
+ };
321
+ // Update the worklog
322
+ await api.put(`/worklogs/${worklogId}`, updatePayload);
323
+ // Information about the update
324
+ let updateInfo = `Worklog updated successfully`;
325
+ // Calculate and show time info if we have a start time
326
+ if (startTime) {
327
+ const endTime = calculateEndTime(startTime, timeSpentHours);
328
+ updateInfo += `. Time logged: ${timeSpentHours} hours starting at ${startTime} and ending at ${endTime}`;
329
+ }
330
+ // Format response
331
+ return {
332
+ content: [
333
+ {
334
+ type: 'text',
335
+ text: updateInfo,
336
+ },
337
+ ],
338
+ };
339
+ }
340
+ catch (error) {
341
+ return {
342
+ isError: true,
343
+ content: [
344
+ { type: 'text', text: `Failed to edit worklog: ${formatError(error)}` },
345
+ ],
346
+ };
347
+ }
348
+ }
349
+ /**
350
+ * Delete a worklog
351
+ */
352
+ export async function deleteWorklog(worklogId) {
353
+ try {
354
+ // Get worklog details for the response
355
+ let worklogDetails = null;
356
+ try {
357
+ const response = await api.get(`/worklogs/${worklogId}`);
358
+ worklogDetails = response.data;
359
+ }
360
+ catch (error) {
361
+ // Continue with deletion even if we can't get details
362
+ console.error(`Could not fetch worklog details: ${error.message}`);
363
+ }
364
+ // Delete the worklog
365
+ await api.delete(`/worklogs/${worklogId}`);
366
+ return {
367
+ content: [{ type: 'text', text: 'Worklog deleted successfully' }],
368
+ };
369
+ }
370
+ catch (error) {
371
+ return {
372
+ isError: true,
373
+ content: [
374
+ {
375
+ type: 'text',
376
+ text: `Failed to delete worklog: ${formatError(error)}`,
377
+ },
378
+ ],
379
+ };
380
+ }
381
+ }
382
+ /**
383
+ * @returns The tempo account associated with the issue via the Jira custom field, if any
384
+ */
385
+ async function fetchTempoAccountFromIssue({ tempoAccountId, }) {
386
+ if (!tempoAccountId)
387
+ return undefined;
388
+ const response = await api.get(`/accounts/${tempoAccountId}`);
389
+ return response.data;
390
+ }
package/build/types.js ADDED
@@ -0,0 +1,78 @@
1
+ import { z } from 'zod';
2
+ // Common validation schemas
3
+ export const dateSchema = () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format');
4
+ export const timeSchema = () => z
5
+ .string()
6
+ .regex(/^([01]\d|2[0-3]):([0-5]\d)$/, 'Time must be in HH:MM format');
7
+ export const issueKeySchema = () => z.string().min(1, 'Issue key cannot be empty');
8
+ export const issueIdSchema = () => z.union([
9
+ z.string().min(1, 'Issue ID cannot be empty'),
10
+ z.number().int().positive('Issue ID must be a positive integer'),
11
+ ]);
12
+ export const idOrKeySchema = () => z.union([issueKeySchema(), issueIdSchema()]);
13
+ // Environment validation
14
+ export const envSchema = z
15
+ .object({
16
+ TEMPO_API_TOKEN: z.string().min(1, 'TEMPO_API_TOKEN is required'),
17
+ JIRA_BASE_URL: z.string().min(1, 'JIRA_BASE_URL is required'),
18
+ JIRA_API_TOKEN: z.string().min(1, 'JIRA_API_TOKEN is required'),
19
+ JIRA_EMAIL: z.string().optional(),
20
+ JIRA_AUTH_TYPE: z.enum(['basic', 'bearer']).optional().default('basic'),
21
+ JIRA_TEMPO_ACCOUNT_CUSTOM_FIELD_ID: z.string().optional(),
22
+ })
23
+ .refine((data) => data.JIRA_AUTH_TYPE === 'bearer' || data.JIRA_EMAIL, {
24
+ message: 'JIRA_EMAIL is required when using basic authentication',
25
+ });
26
+ // Worklog entry schema
27
+ export const worklogEntrySchema = z.object({
28
+ issueKey: issueKeySchema(),
29
+ timeSpentHours: z.number().positive('Time spent must be positive'),
30
+ date: dateSchema(),
31
+ description: z.string().optional(),
32
+ startTime: timeSchema().optional(),
33
+ });
34
+ function getToday() {
35
+ const d = new Date();
36
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
37
+ }
38
+ function getStartOfWeek() {
39
+ const d = new Date();
40
+ const day = d.getDay();
41
+ const diff = day === 0 ? -6 : 1 - day; // back to Monday
42
+ d.setDate(d.getDate() + diff);
43
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
44
+ }
45
+ // MCP tool schemas
46
+ export const retrieveWorklogsSchema = z.object({
47
+ startDate: dateSchema().default(getStartOfWeek),
48
+ endDate: dateSchema().default(getToday),
49
+ });
50
+ export const createWorklogSchema = z.object({
51
+ issueKey: issueKeySchema(),
52
+ timeSpentHours: z
53
+ .number()
54
+ .positive('Time spent must be positive')
55
+ .default(1.5),
56
+ date: dateSchema(),
57
+ description: z.string(),
58
+ startTime: timeSchema().optional().default('08:00'),
59
+ remainingEstimateHours: z
60
+ .number()
61
+ .nonnegative('Remaining estimate must be non-negative')
62
+ .optional(),
63
+ });
64
+ export const bulkCreateWorklogsSchema = z.object({
65
+ worklogEntries: z
66
+ .array(worklogEntrySchema)
67
+ .min(1, 'At least one worklog entry is required'),
68
+ });
69
+ export const editWorklogSchema = z.object({
70
+ worklogId: z.string().min(1, 'Worklog ID is required'),
71
+ timeSpentHours: z.number().positive('Time spent must be positive'),
72
+ description: z.string().optional().nullable(),
73
+ date: dateSchema().optional().nullable(),
74
+ startTime: timeSchema().optional(),
75
+ });
76
+ export const deleteWorklogSchema = z.object({
77
+ worklogId: z.string().min(1, 'Worklog ID is required'),
78
+ });
package/build/utils.js ADDED
@@ -0,0 +1,75 @@
1
+ import axios from 'axios';
2
+ import { getIssueKeyById } from './jira.js';
3
+ /**
4
+ * Standard error handling for API errors
5
+ * Extracts the most useful error message from Axios errors
6
+ */
7
+ export function formatError(error) {
8
+ if (axios.isAxiosError(error)) {
9
+ const data = error.response?.data;
10
+ const status = error.response?.status;
11
+ const parts = [];
12
+ if (status)
13
+ parts.push(`[${status}]`);
14
+ if (data?.errors && Array.isArray(data.errors) && data.errors.length > 0) {
15
+ const errorMessages = data.errors.map((e) => e.message
16
+ ? e.field
17
+ ? `${e.field}: ${e.message}`
18
+ : e.message
19
+ : JSON.stringify(e));
20
+ parts.push(errorMessages.join('; '));
21
+ }
22
+ else if (data?.message) {
23
+ parts.push(data.message);
24
+ }
25
+ else {
26
+ parts.push(error.message);
27
+ }
28
+ return parts.join(' ');
29
+ }
30
+ return error.message;
31
+ }
32
+ /**
33
+ * Get issue keys for worklogs
34
+ * Maps Jira issue IDs to their corresponding issue keys
35
+ */
36
+ export async function getIssueKeysMap(worklogs) {
37
+ // Extract unique issue IDs
38
+ const uniqueIssueIds = [
39
+ ...new Set(worklogs.map((worklog) => worklog.issue?.id).filter((id) => id != null)),
40
+ ];
41
+ if (uniqueIssueIds.length === 0)
42
+ return {};
43
+ // Create issue ID to key map
44
+ const issueIdToKeyMap = {};
45
+ // Fetch issue keys in parallel
46
+ await Promise.all(uniqueIssueIds.map(async (issueId) => {
47
+ try {
48
+ const issueKey = await getIssueKeyById(issueId);
49
+ issueIdToKeyMap[issueId] = issueKey;
50
+ }
51
+ catch (error) {
52
+ console.error(`Could not get key for issue ID ${issueId}: ${error.message}`);
53
+ }
54
+ }));
55
+ return issueIdToKeyMap;
56
+ }
57
+ /**
58
+ * Calculate end time
59
+ * Calculates the end time based on the start time and hours spent
60
+ * @param startTime Time in format HH:MM
61
+ * @param hoursSpent Duration in hours (can be decimal)
62
+ * @returns End time in format HH:MM
63
+ */
64
+ export function calculateEndTime(startTime, hoursSpent) {
65
+ // Parse the HH:MM format
66
+ const [hours, minutes] = startTime.split(':').map((num) => parseInt(num, 10));
67
+ // Create a Date object with today's date but with the given hours and minutes
68
+ const startTimeDate = new Date();
69
+ startTimeDate.setHours(hours, minutes, 0, 0);
70
+ // Add the duration in milliseconds
71
+ const endTimeDate = new Date(startTimeDate.getTime() + hoursSpent * 3600 * 1000);
72
+ // Format the end time as HH:MM
73
+ const endTime = `${endTimeDate.getHours().toString().padStart(2, '0')}:${endTimeDate.getMinutes().toString().padStart(2, '0')}`;
74
+ return endTime;
75
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@fluentdata-ai/tempo-mcp-server",
3
+ "version": "1.3.3",
4
+ "description": "MCP server for managing Tempo worklogs in Jira",
5
+ "main": "build/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "tempo-mcp-server": "build/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
12
+ "start": "node build/index.js",
13
+ "dev": "tsx watch src/index.ts",
14
+ "inspect": "npx @modelcontextprotocol/inspector@latest tsx src/index.ts",
15
+ "prepare": "npm run build && husky",
16
+ "lint": "eslint",
17
+ "format": "prettier . --write",
18
+ "format:check": "prettier . --check"
19
+ },
20
+ "files": [
21
+ "build",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "mcp",
27
+ "tempo",
28
+ "jira",
29
+ "worklogs",
30
+ "time-tracking",
31
+ "claude",
32
+ "cursor",
33
+ "windsurf",
34
+ "cline",
35
+ "ai"
36
+ ],
37
+ "author": "Ivelin Ivanov <ivelinivanov1999@gmail.com>",
38
+ "contributors": [
39
+ "Edgar Isai Salgado Cortez <edgarisaiwr@gmail.com>"
40
+ ],
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/fluentdata-co/tempo-mcp-server.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/fluentdata-co/tempo-mcp-server/issues"
48
+ },
49
+ "homepage": "https://github.com/fluentdata-co/tempo-mcp-server#readme",
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.29.0",
52
+ "axios": "^1.6.7",
53
+ "dotenv": "^17.3.1",
54
+ "zod": "^4.3.6"
55
+ },
56
+ "devDependencies": {
57
+ "@eslint/js": "^9.28.0",
58
+ "@types/node": "^20.11.0",
59
+ "eslint": "^9.28.0",
60
+ "eslint-config-prettier": "^10.1.5",
61
+ "globals": "^16.2.0",
62
+ "husky": "^9.1.7",
63
+ "lint-staged": "^16.1.0",
64
+ "prettier": "3.5.3",
65
+ "tsx": "^4.7.0",
66
+ "typescript": "^5.8.3",
67
+ "typescript-eslint": "^8.34.0"
68
+ },
69
+ "engines": {
70
+ "node": ">=18.0.0"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public"
74
+ },
75
+ "lint-staged": {
76
+ "*.{js,ts}": "eslint --cache --fix --quiet",
77
+ "*": "prettier . --write"
78
+ }
79
+ }