@aot-tech/clockify-mcp-server
Version:
MCP Server for Clockify time tracking integration with AI tools
203 lines (202 loc) • 8.03 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEntryToolWithErrorResponse = exports.listEntriesTool = exports.createEntryTool = exports.extractErrorMessage = void 0;
const zod_1 = require("zod");
const api_1 = require("../config/api");
const entries_1 = require("../clockify-sdk/entries");
const extractErrorMessage = (error) => {
if (error.name === 'AxiosError' || error.isAxiosError) {
if (error.response?.data) {
const { status, data } = error.response;
if (data.message) {
const errorCode = data.code ? ` (Code: ${data.code})` : '';
return `${data.message}${errorCode}`;
}
if (data.error) {
return data.error;
}
if (data.errors && Array.isArray(data.errors)) {
return data.errors.join(', ');
}
switch (status) {
case 401:
return 'Authentication failed. Please check your API credentials.';
case 403:
return 'Access denied. Check your permissions for this resource.';
case 404:
return 'Resource not found. Please verify the provided IDs.';
case 429:
return 'Rate limit exceeded. Please try again later.';
case 500:
return 'Internal server error. Please try again later.';
default:
return `HTTP ${status}: ${JSON.stringify(data)}`;
}
}
if (error.code) {
switch (error.code) {
case 'ERR_BAD_REQUEST':
return 'Bad request. Please check your request parameters.';
case 'ECONNREFUSED':
return 'Connection refused. Please check if the service is running.';
case 'ENOTFOUND':
return 'Network error. Please check your internet connection.';
case 'ETIMEDOUT':
return 'Request timeout. Please try again.';
default:
return `Network error: ${error.code}`;
}
}
}
return error.message || 'An unexpected error occurred';
};
exports.extractErrorMessage = extractErrorMessage;
exports.createEntryTool = {
name: api_1.TOOLS_CONFIG.entries.create.name,
description: api_1.TOOLS_CONFIG.entries.create.description,
parameters: {
workspaceId: zod_1.z
.string()
.describe('The id of the workspace that gonna be saved the time entry'),
billable: zod_1.z
.boolean()
.describe('If the task is billable or not')
.optional()
.default(true),
description: zod_1.z.string().describe('The description of the time entry'),
start: zod_1.z.coerce.date().describe('The start of the time entry'),
end: zod_1.z.coerce.date().describe('The end of the time entry'),
projectId: zod_1.z
.string()
.optional()
.describe('The id of the project associated with this time entry'),
},
handler: async (params) => {
try {
const result = await entries_1.entriesService.create(params);
const entryInfo = `Entry successfully created. ID: ${result.data.id} Name: ${result.data.description}`;
return {
content: [
{
type: 'text',
text: entryInfo,
},
],
};
}
catch (error) {
const errorMessage = (0, exports.extractErrorMessage)(error);
return {
content: [
{
type: 'text',
text: `Error creating entry: ${errorMessage}`,
},
],
};
}
},
};
exports.listEntriesTool = {
name: api_1.TOOLS_CONFIG.entries.list.name,
description: api_1.TOOLS_CONFIG.entries.list.description,
parameters: {
workspaceId: zod_1.z
.string()
.describe('The id of the workspace that gonna search for the entries. This is required to get the time entries .use clockify_get_workspaces tool to get the workspace id'),
userId: zod_1.z
.string()
.describe('REQUIRED: Exact Clockify user ID (UUID format like "507f1f77bcf86cd799439011"). NEVER use "all", "user", or any placeholder. Must be a real user ID from Clockify workspace.'),
start: zod_1.z.coerce
.date()
.describe('Start time of the entry to search for'),
end: zod_1.z.coerce
.date()
.describe('End time of the entry to search for'),
page: zod_1.z.number().describe('The page number to fetch'),
pageSize: zod_1.z.number().describe('The number of results per page'),
},
handler: async (params) => {
try {
const result = await entries_1.entriesService.find(params);
const formmatedResults = result?.entries?.data?.map((entry) => ({
id: entry?.id,
description: entry?.description,
timeInterval: entry?.timeInterval || '',
billable: entry?.billable || false,
hourlyRate: entry?.hourlyRate || 0,
costRate: entry?.costRate || 0,
}));
const formattedResultsWithHasNextPage = {
...formmatedResults,
hasNextPage: result?.hasNextPage
};
return {
content: [
{
type: 'text',
text: `Clockify Time Entries ${JSON.stringify(formattedResultsWithHasNextPage, null, 2)}`,
},
],
};
}
catch (error) {
const errorMessage = (0, exports.extractErrorMessage)(error);
return {
content: [
{
type: 'text',
text: `Error retrieving entries: ${errorMessage}`,
},
],
};
}
},
};
exports.createEntryToolWithErrorResponse = {
name: api_1.TOOLS_CONFIG.entries.create.name,
description: api_1.TOOLS_CONFIG.entries.create.description,
parameters: {
workspaceId: zod_1.z
.string()
.describe('The id of the workspace that gonna be saved the time entry'),
billable: zod_1.z
.boolean()
.describe('If the task is billable or not')
.optional()
.default(true),
description: zod_1.z.string().describe('The description of the time entry'),
start: zod_1.z.coerce.date().describe('The start of the time entry'),
end: zod_1.z.coerce.date().describe('The end of the time entry'),
projectId: zod_1.z
.string()
.optional()
.describe('The id of the project associated with this time entry'),
},
handler: async (params) => {
try {
const result = await entries_1.entriesService.create(params);
const entryInfo = `Entry successfully created. ID: ${result.data.id} Name: ${result.data.description}`;
return {
content: [
{
type: 'text',
text: entryInfo,
},
],
};
}
catch (error) {
const errorMessage = (0, exports.extractErrorMessage)(error);
return {
content: [
{
type: 'text',
text: `Error creating entry: ${errorMessage}`,
},
],
isError: true,
};
}
},
};