import { Injectable } from '@nestjs/common';
import { Tool } from '@rekog/mcp-nest';
import { z } from 'zod';
import { AuthService } from '@/auth/auth.service';
import { TimesheetsService } from './timesheets.service';
import { ApiClientService } from '@/shared/http/api-client.service';
import { ResponseUtil } from '@/common/utils/response.util';
/**
* MCP Tools for timesheet management
* Provides descriptive tool names for LLM decision making:
* - create_new_timesheet_entry: Create a new daily timesheet entry with work details
* - list_user_timesheet_entries: Fetch and list user's timesheet entries with pagination
* - fetch_available_work_activities: Get all available work activities/task types
*/
@Injectable()
export class TimesheetsTools {
constructor(
private readonly authService: AuthService,
private readonly timesheetsService: TimesheetsService,
private readonly apiClient: ApiClientService,
) {}
/**
* Authenticate using environment credentials if no token exists
* Used by all tools to ensure authentication
*/
private async ensureAuthenticated(): Promise<{ user: any; token: string } | null> {
try {
// Try to get existing token
let user = this.authService.getCurrentUser();
let token = this.authService.getToken();
// Try to refresh token
try {
const refreshedToken = await this.authService.refreshToken();
if (refreshedToken) {
token = refreshedToken;
try {
user = this.authService.getCurrentUser();
} catch (e) {
// Continue with existing user
}
}
} catch (error) {
// Token refresh failed, try to login with env credentials
return await this.authenticateWithEnvCredentials();
}
return { user, token };
} catch (error) {
// No token in config, try to login with env credentials
return await this.authenticateWithEnvCredentials();
}
}
/**
* Authenticate using environment variables
*/
private async authenticateWithEnvCredentials(): Promise<{ user: any; token: string } | null> {
const username = process.env.TIMESHEET_USERNAME;
const password = process.env.TIMESHEET_PASSWORD;
if (!username || !password) {
return null;
}
try {
const loginResult = await this.authService.login(username, password);
if (!loginResult.success || !loginResult.user) {
return null;
}
try {
const user = this.authService.getCurrentUser();
const token = this.authService.getToken();
return { user, token };
} catch (error) {
return null;
}
} catch (error) {
return null;
}
}
@Tool({
name: 'create_new_timesheet_entry',
description:
'Create a new daily timesheet entry. Records work done for a specific date, including start/end time, project, module, activity type, and work description. Minimum description length is 10 characters. Automatically handles authentication with fresh token. Calls: POST /api/timesheets/create',
parameters: z.object({
project_id: z.number().describe('Project ID - which project the work belongs to'),
module_id: z.number().describe('Module ID - which module/component within the project'),
activity_id: z.number().describe('Activity ID - the type of work (e.g., Development, Code Review, Testing)'),
start_time: z.string().describe('Start time in HH:MM:SS format (e.g., 09:00:00)'),
end_time: z.string().describe('End time in HH:MM:SS format (e.g., 10:30:00)'),
duration: z.number().describe('Duration in minutes (total work time for this entry)'),
description: z
.string()
.min(10)
.describe('Detailed work description (minimum 10 characters) - what was accomplished'),
date: z.string().describe('Date in YYYY-MM-DD format (e.g., 2025-11-04)'),
}),
})
async createNewTimesheetEntry(params: {
project_id: number;
module_id: number;
activity_id: number;
start_time: string;
end_time: string;
duration: number;
description: string;
date: string;
}) {
try {
const auth = await this.ensureAuthenticated();
if (!auth || !auth.user || !auth.token) {
const username = process.env.TIMESHEET_USERNAME;
const password = process.env.TIMESHEET_PASSWORD;
if (!username || !password) {
return ResponseUtil.error(
'User not authenticated. Please configure TIMESHEET_USERNAME and TIMESHEET_PASSWORD in your MCP client environment variables.',
);
}
return ResponseUtil.error(
'Authentication failed: Invalid username or password. Please verify your credentials in the MCP client configuration.',
);
}
const result = await this.timesheetsService.createTimesheetEntry(params);
return ResponseUtil.success(
'✓ Daily timesheet entry created successfully!',
result,
);
} catch (error) {
return ResponseUtil.error(
error.message || 'Failed to create daily timesheet entry',
);
}
}
@Tool({
name: 'list_user_timesheet_entries',
description:
'Fetch and display user\'s timesheet entries with pagination support. Shows previously recorded work entries. Default limit is 10 entries per page, maximum 100. Automatically handles authentication with fresh token. Calls: GET /api/timesheets/list/{userId}',
parameters: z.object({
page: z
.number()
.optional()
.default(1)
.describe('Page number for pagination (default: 1)'),
limit: z
.number()
.optional()
.default(10)
.describe('Number of entries per page (default: 10, max: 100)'),
}),
})
async listUserTimesheetEntries(params: { page?: number; limit?: number }) {
try {
const auth = await this.ensureAuthenticated();
if (!auth || !auth.user || !auth.token) {
const username = process.env.TIMESHEET_USERNAME;
const password = process.env.TIMESHEET_PASSWORD;
if (!username || !password) {
return ResponseUtil.error(
'User not authenticated. Please configure TIMESHEET_USERNAME and TIMESHEET_PASSWORD in your MCP client environment variables.',
);
}
return ResponseUtil.error(
'Authentication failed: Invalid username or password. Please verify your credentials in the MCP client configuration.',
);
}
const { data, pagination } = await this.timesheetsService.fetchTimesheetEntries(params);
return ResponseUtil.success(
'✓ Timesheet entries fetched successfully!',
data,
pagination,
);
} catch (error) {
return ResponseUtil.error(
error.message || 'Failed to fetch timesheet entries',
);
}
}
}