feat: add user retrieval functions and schemas for GitLab API integration

This commit is contained in:
Martim Pimentel
2025-05-21 22:18:06 +01:00
parent 808c34d0ee
commit 005b46a1a6
2 changed files with 105 additions and 8 deletions

View File

@ -37,6 +37,9 @@ import {
GitLabNamespaceExistsResponseSchema, GitLabNamespaceExistsResponseSchema,
GitLabProjectSchema, GitLabProjectSchema,
GitLabLabelSchema, GitLabLabelSchema,
GitLabUserSchema,
GitLabUsersResponseSchema,
GetUsersSchema,
CreateRepositoryOptionsSchema, CreateRepositoryOptionsSchema,
CreateIssueOptionsSchema, CreateIssueOptionsSchema,
CreateMergeRequestOptionsSchema, CreateMergeRequestOptionsSchema,
@ -108,6 +111,8 @@ import {
type GitLabNamespaceExistsResponse, type GitLabNamespaceExistsResponse,
type GitLabProject, type GitLabProject,
type GitLabLabel, type GitLabLabel,
type GitLabUser,
type GitLabUsersResponse,
// Discussion Types // Discussion Types
type GitLabDiscussionNote, // Added type GitLabDiscussionNote, // Added
type GitLabDiscussion, type GitLabDiscussion,
@ -418,6 +423,11 @@ const allTools = [
"Get the repository tree for a GitLab project (list files and directories)", "Get the repository tree for a GitLab project (list files and directories)",
inputSchema: zodToJsonSchema(GetRepositoryTreeSchema), inputSchema: zodToJsonSchema(GetRepositoryTreeSchema),
}, },
{
name: "get_users",
description: "Get GitLab user details by usernames",
inputSchema: zodToJsonSchema(GetUsersSchema),
},
]; ];
// Define which tools are read-only // Define which tools are read-only
@ -440,6 +450,7 @@ const readOnlyTools = [
"list_labels", "list_labels",
"get_label", "get_label",
"list_group_projects", "list_group_projects",
"get_users",
]; ];
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@ -2255,6 +2266,65 @@ async function getRepositoryTree(
return z.array(GitLabTreeItemSchema).parse(data); return z.array(GitLabTreeItemSchema).parse(data);
} }
/**
* Get a single user from GitLab
*
* @param {string} username - The username to look up
* @returns {Promise<GitLabUser | null>} The user data or null if not found
*/
async function getUser(username: string): Promise<GitLabUser | null> {
try {
const url = new URL(`${GITLAB_API_URL}/users`);
url.searchParams.append("username", username);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const users = await response.json();
// GitLab returns an array of users that match the username
if (Array.isArray(users) && users.length > 0) {
// Find exact match for username (case-sensitive)
const exactMatch = users.find(user => user.username === username);
if (exactMatch) {
return GitLabUserSchema.parse(exactMatch);
}
}
// No matching user found
return null;
} catch (error) {
console.error(`Error fetching user by username '${username}':`, error);
return null;
}
}
/**
* Get multiple users from GitLab
*
* @param {string[]} usernames - Array of usernames to look up
* @returns {Promise<GitLabUsersResponse>} Object with usernames as keys and user objects or null as values
*/
async function getUsers(usernames: string[]): Promise<GitLabUsersResponse> {
const users: Record<string, GitLabUser | null> = {};
// Process usernames sequentially to avoid rate limiting
for (const username of usernames) {
try {
const user = await getUser(username);
users[username] = user;
} catch (error) {
console.error(`Error processing username '${username}':`, error);
users[username] = null;
}
}
return GitLabUsersResponseSchema.parse(users);
}
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
// Apply read-only filter first // Apply read-only filter first
const tools0 = GITLAB_READ_ONLY_MODE const tools0 = GITLAB_READ_ONLY_MODE
@ -2621,6 +2691,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
}; };
} }
case "get_users": {
const args = GetUsersSchema.parse(request.params.arguments);
const usersMap = await getUsers(args.usernames);
return {
content: [{ type: "text", text: JSON.stringify(usersMap, null, 2) }],
};
}
case "create_note": { case "create_note": {
const args = CreateNoteSchema.parse(request.params.arguments); const args = CreateNoteSchema.parse(request.params.arguments);

View File

@ -7,6 +7,30 @@ export const GitLabAuthorSchema = z.object({
date: z.string(), date: z.string(),
}); });
// User schemas
export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
name: z.string(),
avatar_url: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GetUsersSchema = z.object({
usernames: z.array(z.string()).describe("Array of usernames to search for"),
});
export const GitLabUsersResponseSchema = z.record(
z.string(),
z.object({
id: z.number(),
username: z.string(),
name: z.string(),
avatar_url: z.string(),
web_url: z.string(),
}).nullable()
);
// Namespace related schemas // Namespace related schemas
// Base schema for project-related operations // Base schema for project-related operations
@ -283,14 +307,6 @@ export const GitLabLabelSchema = z.object({
is_project_label: z.boolean().optional(), is_project_label: z.boolean().optional(),
}); });
export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
name: z.string(),
avatar_url: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabMilestoneSchema = z.object({ export const GitLabMilestoneSchema = z.object({
id: z.number(), id: z.number(),
iid: z.number(), // Added to match GitLab API iid: z.number(), // Added to match GitLab API
@ -1103,3 +1119,5 @@ export type GetRepositoryTreeOptions = z.infer<typeof GetRepositoryTreeSchema>;
export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>; export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>;
export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>; export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>;
export type CreateMergeRequestNoteOptions = z.infer<typeof CreateMergeRequestNoteSchema>; export type CreateMergeRequestNoteOptions = z.infer<typeof CreateMergeRequestNoteSchema>;
export type GitLabUser = z.infer<typeof GitLabUserSchema>;
export type GitLabUsersResponse = z.infer<typeof GitLabUsersResponseSchema>;