import { Injectable } from '@nestjs/common';
import { Tool } from '@rekog/mcp-nest';
import { z } from 'zod';
import { AuthService } from './auth.service';
import { ApiClientService } from '@/shared/http/api-client.service';
import { ResponseUtil } from '@/common/utils/response.util';
/**
* MCP Tools for user authentication
* Provides comprehensive tool for user info, projects, and modules
*/
@Injectable()
export class AuthTools {
constructor(
private readonly authService: AuthService,
private readonly apiClient: ApiClientService,
) {}
@Tool({
name: 'whoami',
description:
'Get current authenticated user information with automatically fetched projects and modules. Returns user details (name, email, roles, manager), their assigned projects, and modules within those projects. Automatically refreshes authentication token on every call. This is the primary tool for verifying authentication and discovering available work contexts. Calls: POST /api/auth/refresh, GET /api/projects/user/{userId}, and GET /api/modules/by-project?projectId={id}',
parameters: z.object({}),
})
async whoami() {
try {
let user: any;
let token: string;
// Try to get existing user and token from config
try {
user = this.authService.getCurrentUser();
token = this.authService.getToken();
} catch (error) {
// No token in config, try to login with env credentials
return await this.authenticateAndFetchUserData();
}
// If we have token, try to refresh it to get fresh token
if (user && token) {
try {
const refreshedToken = await this.authService.refreshToken();
if (refreshedToken) {
token = refreshedToken;
// Also get updated user from config
try {
user = this.authService.getCurrentUser();
} catch (e) {
// Continue with existing user if update fails
}
}
} catch (error) {
// Token refresh failed (possibly expired), try to re-authenticate with env credentials
return await this.authenticateAndFetchUserData();
}
}
// Now fetch user projects and modules with the token
return await this.fetchUserDataWithToken(user, token);
} catch (error) {
return ResponseUtil.error(
`Unexpected error: ${error.message || 'Failed to retrieve user information'}`,
);
}
}
/**
* Authenticate using environment credentials and fetch user data
* Called when no token exists in config or token refresh fails
*/
private async authenticateAndFetchUserData(): Promise<any> {
const username = process.env.TIMESHEET_USERNAME;
const password = process.env.TIMESHEET_PASSWORD;
// Check if credentials are configured
if (!username || !password) {
return ResponseUtil.error(
'User not authenticated. Please configure TIMESHEET_USERNAME and TIMESHEET_PASSWORD in your MCP client environment variables.',
);
}
// Try to login with env credentials
try {
const loginResult = await this.authService.login(username, password);
if (!loginResult.success || !loginResult.user) {
return ResponseUtil.error(
'Authentication failed: Invalid username or password. Please verify your credentials in the MCP client configuration.',
);
}
// Login successful, get fresh user and token from config
try {
const user = this.authService.getCurrentUser();
const token = this.authService.getToken();
return await this.fetchUserDataWithToken(user, token);
} catch (error) {
return ResponseUtil.error(
`Authentication succeeded but failed to retrieve user data: ${error.message}`,
);
}
} catch (error) {
// Login failed
const errorMessage = error.message || 'Unknown error';
if (errorMessage.includes('Invalid credentials') || errorMessage.includes('invalid')) {
return ResponseUtil.error(
'Authentication failed: Invalid username or password. Please verify your TIMESHEET_USERNAME and TIMESHEET_PASSWORD in the MCP client configuration.',
);
} else if (errorMessage.includes('No response') || errorMessage.includes('ECONNREFUSED')) {
return ResponseUtil.error(
'Cannot connect to timesheet API. Please verify the API is accessible and check your network connection.',
);
} else {
return ResponseUtil.error(
`Authentication error: ${errorMessage}. Please verify your credentials and API connection.`,
);
}
}
}
/**
* Fetch user projects and modules with authenticated token
*/
private async fetchUserDataWithToken(user: any, token: string): Promise<any> {
// Fetch user projects and modules
let projectsResponse = null;
let modulesResponse = null;
try {
projectsResponse = await this.apiClient.getUserProjects(user.id, token);
// If user has projects, fetch modules for the first project
if (projectsResponse?.data && projectsResponse.data.length > 0) {
const firstProject = projectsResponse.data[0];
const projectId = firstProject.id || firstProject.project_id;
if (projectId) {
modulesResponse = await this.apiClient.getModules(projectId, token);
}
}
} catch (error) {
// Log error but don't fail the entire whoami call
console.warn('Failed to fetch projects/modules:', error.message);
}
return ResponseUtil.success('✓ User authenticated successfully! (Token refreshed)', {
user: {
id: user.id,
name: user.name,
username: user.username,
email: user.email,
roles: user.roles || [],
managerId: user.managerId,
},
projects: projectsResponse?.data || [],
modules: modulesResponse?.data
? modulesResponse.data.map((module: any) => ({
id: module.module_id,
name: module.module_name,
project_id: module.project_id,
}))
: [],
});
}
}