Skip to main content
Glama
liratanak

Tonle OpenProject MCP Server

by liratanak
server-setup.ts55.8 kB
#!/usr/bin/env bun /** * OpenProject MCP Server Setup * Shared server configuration for both STDIO and HTTP transports */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { createClient, type OpenProjectClient } from './openproject-client.ts'; import logger from './logger.ts'; export interface ServerConfig { name?: string; version?: string; } // Helper to safely stringify responses export function formatResponse(data: unknown): string { return JSON.stringify(data, null, 2); } // Helper to create API links export function createLink(type: string, id: number | string): string { return `/api/v3/${type}/${id}`; } export async function resolveProjectId(client: OpenProjectClient, projectRef: number | string): Promise<number> { if (typeof projectRef === 'number') { return projectRef; } const numericId = Number(projectRef); if (!Number.isNaN(numericId)) { return numericId; } const project = await client.getProject(projectRef); return project.id; } export function buildProjectMembershipFilter(projectId: number): string { return JSON.stringify([ { project: { operator: '=', values: [String(projectId)], }, }, ]); } export function extractResourceId(href: string, resource: string): number | null { const regex = new RegExp(`/${resource}/(\\d+)(?:/|$)`); const match = href.match(regex); return match ? Number(match[1]) : null; } // Helper to wrap tool handlers with logging function wrapToolHandler<T extends z.ZodTypeAny>( client: OpenProjectClient, toolName: string, handler: (params: z.infer<T>) => Promise<any> ): (params: z.infer<T>) => Promise<any> { return async (params: z.infer<T>) => { const caller = `tool:${toolName}`; // Log tool invocation logger.logToolInvocation(caller, toolName, params); // Set caller in client for API logging client.setCaller(caller); try { const result = await handler(params); // Log successful tool result logger.logToolResult(caller, toolName, true, result); return result; } catch (error) { // Log failed tool result logger.logToolResult(caller, toolName, false, undefined, error as Error); throw error; } }; } export function setupMcpServer(config: ServerConfig = {}): { server: McpServer; initClient: () => Promise<OpenProjectClient> } { const server = new McpServer({ name: config.name || 'openproject-mcp', version: config.version || '1.0.0', }); let client: OpenProjectClient; const initClient = async (): Promise<OpenProjectClient> => { client = createClient('system'); return client; }; // ============== Root & Configuration Tools ============== server.tool( 'get_api_root', 'Get the OpenProject API root information', {}, async () => { const toolName = 'get_api_root'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, {}); client.setCaller(caller); try { const result = await client.getRoot(); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_configuration', 'Get the OpenProject instance configuration', {}, async () => { const toolName = 'get_configuration'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, {}); client.setCaller(caller); try { const result = await client.getConfiguration(); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Project Tools ============== server.tool( 'list_projects', 'List all projects accessible to the current user', { offset: z.number().optional().describe('Page offset for pagination (default: 0)'), pageSize: z.number().optional().describe('Number of items per page (default: 20, max: 1000)'), filters: z.string().optional().describe('JSON filter expression'), sortBy: z.string().optional().describe('Sort criteria as JSON array'), }, async (params) => { const toolName = 'list_projects'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.listProjects(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_project', 'Get details of a specific project', { id: z.union([z.number(), z.string()]).describe('Project ID or identifier'), }, async ({ id }) => { const toolName = 'get_project'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getProject(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'create_project', 'Create a new project', { name: z.string().describe('Name of the project'), identifier: z.string().optional().describe('Unique identifier (auto-generated if not provided)'), description: z.string().optional().describe('Project description'), public: z.boolean().optional().describe('Whether the project is public (default: false)'), status: z.enum(['on_track', 'at_risk', 'off_track', 'not_set']).optional().describe('Project status'), statusExplanation: z.string().optional().describe('Explanation for the project status'), parentId: z.number().optional().describe('Parent project ID'), }, async ({ name, identifier, description, public: isPublic, status, statusExplanation, parentId }) => { const toolName = 'create_project'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { name, identifier, description, public: isPublic, status, statusExplanation, parentId }); client.setCaller(caller); try { const data: Parameters<typeof client.createProject>[0] = { name, identifier, public: isPublic, status, }; if (description) data.description = { raw: description }; if (statusExplanation) data.statusExplanation = { raw: statusExplanation }; if (parentId) data.parent = { href: createLink('projects', parentId) }; const result = await client.createProject(data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'update_project', 'Update an existing project', { id: z.union([z.number(), z.string()]).describe('Project ID or identifier'), name: z.string().optional().describe('New name for the project'), description: z.string().optional().describe('New project description'), public: z.boolean().optional().describe('Whether the project is public'), active: z.boolean().optional().describe('Whether the project is active'), status: z.enum(['on_track', 'at_risk', 'off_track', 'not_set']).optional().describe('Project status'), statusExplanation: z.string().optional().describe('Explanation for the project status'), }, async ({ id, name, description, public: isPublic, active, status, statusExplanation }) => { const toolName = 'update_project'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id, name, description, public: isPublic, active, status, statusExplanation }); client.setCaller(caller); try { const data: Parameters<typeof client.updateProject>[1] = { name, public: isPublic, active, status, }; if (description !== undefined) data.description = { raw: description }; if (statusExplanation !== undefined) data.statusExplanation = { raw: statusExplanation }; const result = await client.updateProject(id, data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'delete_project', 'Delete a project (requires confirmation)', { id: z.union([z.number(), z.string()]).describe('Project ID or identifier'), }, async ({ id }) => { const toolName = 'delete_project'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { await client.deleteProject(id); return { content: [{ type: 'text', text: `Project ${id} deleted successfully` }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Work Package Tools ============== server.tool( 'list_work_packages', 'List all work packages with optional filtering', { offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), sortBy: z.string().optional().describe('Sort criteria as JSON array'), groupBy: z.string().optional().describe('Group by attribute'), query_id: z.number().optional().describe('Query ID to apply a saved query/filter'), }, async (params) => { const toolName = 'list_work_packages'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.listWorkPackages(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'list_project_work_packages', 'List work packages in a specific project', { projectId: z.union([z.number(), z.string()]).describe('Project ID or identifier'), offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), sortBy: z.string().optional().describe('Sort criteria as JSON array'), query_id: z.number().optional().describe('Query ID to apply a saved query/filter'), }, async ({ projectId, ...params }) => { const toolName = 'list_project_work_packages'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { projectId, ...params }); client.setCaller(caller); try { const result = await client.listProjectWorkPackages(projectId, params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_work_package', 'Get details of a specific work package', { id: z.number().describe('Work package ID'), }, async ({ id }) => { const toolName = 'get_work_package'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getWorkPackage(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'create_work_package', 'Create a new work package in a project', { projectId: z.union([z.number(), z.string()]).describe('Project ID or identifier'), subject: z.string().describe('Subject/title of the work package'), description: z.string().optional().describe('Detailed description (supports markdown)'), typeId: z.number().optional().describe('Work package type ID'), statusId: z.number().optional().describe('Status ID'), priorityId: z.number().optional().describe('Priority ID'), assigneeId: z.number().optional().describe('Assignee user ID'), responsibleId: z.number().optional().describe('Responsible user ID'), versionId: z.number().optional().describe('Version/milestone ID'), parentId: z.number().optional().describe('Parent work package ID'), startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), dueDate: z.string().optional().describe('Due date (YYYY-MM-DD)'), estimatedTime: z.string().optional().describe('Estimated time (ISO 8601 duration, e.g., PT8H)'), percentageDone: z.number().min(0).max(100).optional().describe('Completion percentage (0-100)'), notify: z.boolean().optional().describe('Send notifications (default: true)'), }, async ({ projectId, subject, description, typeId, statusId, priorityId, assigneeId, responsibleId, versionId, parentId, startDate, dueDate, estimatedTime, percentageDone, notify }) => { const toolName = 'create_work_package'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { projectId, subject, description, typeId, statusId, priorityId, assigneeId, responsibleId, versionId, parentId, startDate, dueDate, estimatedTime, percentageDone, notify }); client.setCaller(caller); try { const _links: NonNullable<Parameters<typeof client.createWorkPackage>[1]['_links']> = {}; if (typeId) _links.type = { href: createLink('types', typeId) }; if (statusId) _links.status = { href: createLink('statuses', statusId) }; if (priorityId) _links.priority = { href: createLink('priorities', priorityId) }; if (assigneeId) _links.assignee = { href: createLink('users', assigneeId) }; if (responsibleId) _links.responsible = { href: createLink('users', responsibleId) }; if (versionId) _links.version = { href: createLink('versions', versionId) }; if (parentId) _links.parent = { href: createLink('work_packages', parentId) }; const data: Parameters<typeof client.createWorkPackage>[1] = { subject, _links: Object.keys(_links).length > 0 ? _links : undefined, startDate, dueDate, estimatedTime, percentageDone, }; if (description) data.description = { raw: description }; const result = await client.createWorkPackage(projectId, data, notify); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'update_work_package', 'Update an existing work package', { id: z.number().describe('Work package ID'), lockVersion: z.number().describe('Current lock version (for optimistic locking)'), subject: z.string().optional().describe('New subject/title'), description: z.string().optional().describe('New description'), typeId: z.number().optional().describe('New type ID'), statusId: z.number().optional().describe('New status ID'), priorityId: z.number().optional().describe('New priority ID'), assigneeId: z.number().optional().describe('New assignee user ID'), responsibleId: z.number().optional().describe('New responsible user ID'), versionId: z.number().optional().describe('New version ID'), parentId: z.number().optional().describe('New parent work package ID'), startDate: z.string().optional().describe('New start date (YYYY-MM-DD)'), dueDate: z.string().optional().describe('New due date (YYYY-MM-DD)'), estimatedTime: z.string().optional().describe('New estimated time'), percentageDone: z.number().min(0).max(100).optional().describe('New completion percentage'), notify: z.boolean().optional().describe('Send notifications'), }, async ({ id, lockVersion, subject, description, typeId, statusId, priorityId, assigneeId, responsibleId, versionId, parentId, startDate, dueDate, estimatedTime, percentageDone, notify }) => { const toolName = 'update_work_package'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id, lockVersion, subject, description, typeId, statusId, priorityId, assigneeId, responsibleId, versionId, parentId, startDate, dueDate, estimatedTime, percentageDone, notify }); client.setCaller(caller); try { const _links: NonNullable<Parameters<typeof client.updateWorkPackage>[1]['_links']> = {}; if (typeId) _links.type = { href: createLink('types', typeId) }; if (statusId) _links.status = { href: createLink('statuses', statusId) }; if (priorityId) _links.priority = { href: createLink('priorities', priorityId) }; if (assigneeId) _links.assignee = { href: createLink('users', assigneeId) }; if (responsibleId) _links.responsible = { href: createLink('users', responsibleId) }; if (versionId) _links.version = { href: createLink('versions', versionId) }; if (parentId) _links.parent = { href: createLink('work_packages', parentId) }; const data: Parameters<typeof client.updateWorkPackage>[1] = { lockVersion, subject, _links: Object.keys(_links).length > 0 ? _links : undefined, startDate, dueDate, estimatedTime, percentageDone, }; if (description !== undefined) data.description = { raw: description }; const result = await client.updateWorkPackage(id, data, notify); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'delete_work_package', 'Delete a work package', { id: z.number().describe('Work package ID'), }, async ({ id }) => { const toolName = 'delete_work_package'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { await client.deleteWorkPackage(id); return { content: [{ type: 'text', text: `Work package ${id} deleted successfully` }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'list_work_package_activities', 'List activities/journal entries for a work package', { id: z.number().describe('Work package ID'), }, async ({ id }) => { const toolName = 'list_work_package_activities'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.listWorkPackageActivities(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== User Tools ============== server.tool( 'list_users', 'List all users (administrator only)', { offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), sortBy: z.string().optional().describe('Sort criteria as JSON array'), }, async (params) => { const toolName = 'list_users'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const currentUser = await client.getCurrentUser(); if (!currentUser.admin) { return { content: [{ type: 'text', text: 'Error: list_users requires administrator privileges' }], isError: true, }; } const result = await client.listUsers(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_user', 'Get details of a specific user', { id: z.union([z.number(), z.string()]).describe('User ID or "me" for current user'), }, async ({ id }) => { const toolName = 'get_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = id === 'me' ? await client.getCurrentUser() : await client.getUser(id); return { content: [{ type: 'text', text: formatResponse(result) }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_current_user', 'Get the currently authenticated user', {}, async () => { const toolName = 'get_current_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, {}); client.setCaller(caller); try { const result = await client.getCurrentUser(); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'create_user', 'Create a new user (admin only)', { login: z.string().describe('Login username'), email: z.string().email().describe('Email address'), firstName: z.string().describe('First name'), lastName: z.string().describe('Last name'), admin: z.boolean().optional().describe('Whether user is admin'), language: z.string().optional().describe('Preferred language'), password: z.string().optional().describe('Initial password'), }, async (params) => { const toolName = 'create_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.createUser(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'update_user', 'Update an existing user', { id: z.number().describe('User ID'), login: z.string().optional().describe('New login username'), email: z.string().email().optional().describe('New email address'), firstName: z.string().optional().describe('New first name'), lastName: z.string().optional().describe('New last name'), admin: z.boolean().optional().describe('Admin status'), language: z.string().optional().describe('Preferred language'), }, async ({ id, ...data }) => { const toolName = 'update_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id, ...data }); client.setCaller(caller); try { const result = await client.updateUser(id, data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'delete_user', 'Delete a user (admin only)', { id: z.number().describe('User ID'), }, async ({ id }) => { const toolName = 'delete_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { await client.deleteUser(id); return { content: [{ type: 'text', text: `User ${id} deleted successfully` }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'lock_user', 'Lock a user account', { id: z.number().describe('User ID'), }, async ({ id }) => { const toolName = 'lock_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.lockUser(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'unlock_user', 'Unlock a user account', { id: z.number().describe('User ID'), }, async ({ id }) => { const toolName = 'unlock_user'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.unlockUser(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Membership Tools ============== server.tool( 'list_memberships', 'List project memberships (users/groups assigned to projects)', { offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), sortBy: z.string().optional().describe('Sort criteria as JSON array'), }, async (params) => { const toolName = 'list_memberships'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.listMemberships(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'list_project_members', 'List members who belong to a specific project', { projectId: z.union([z.number(), z.string()]).describe('Project ID or identifier'), offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), }, async ({ projectId, offset, pageSize }) => { const toolName = 'list_project_members'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { projectId, offset, pageSize }); client.setCaller(caller); try { const resolvedProjectId = await resolveProjectId(client, projectId); const filters = buildProjectMembershipFilter(resolvedProjectId); const result = await client.listMemberships({ offset, pageSize, filters }); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'list_work_package_members', 'List members of the project that owns a work package', { workPackageId: z.number().describe('Work package ID'), offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), }, async ({ workPackageId, offset, pageSize }) => { const toolName = 'list_work_package_members'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { workPackageId, offset, pageSize }); client.setCaller(caller); try { const workPackage = await client.getWorkPackage(workPackageId); const projectHref = workPackage._links?.project?.href; if (!projectHref) { throw new Error(`Work package ${workPackageId} does not reference a project.`); } const projectId = extractResourceId(projectHref, 'projects'); if (projectId === null) { throw new Error(`Unable to extract project ID from link: ${projectHref}`); } const filters = buildProjectMembershipFilter(projectId); const memberships = await client.listMemberships({ offset, pageSize, filters }); const response = { workPackageId, projectId, projectHref, memberships, }; return { content: [{ type: 'text', text: formatResponse(response) }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Type Tools ============== server.tool( 'list_types', 'List all work package types', {}, async () => { const toolName = 'list_types'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, {}); client.setCaller(caller); try { const result = await client.listTypes(); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_type', 'Get details of a specific work package type', { id: z.number().describe('Type ID'), }, async ({ id }) => { const toolName = 'get_type'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getType(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'list_project_types', 'List types available in a specific project', { projectId: z.union([z.number(), z.string()]).describe('Project ID or identifier'), }, async ({ projectId }) => { const toolName = 'list_project_types'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { projectId }); client.setCaller(caller); try { const result = await client.listProjectTypes(projectId); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Status Tools ============== server.tool( 'list_statuses', 'List all work package statuses', {}, async () => { const toolName = 'list_statuses'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, {}); client.setCaller(caller); try { const result = await client.listStatuses(); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_status', 'Get details of a specific status', { id: z.number().describe('Status ID'), }, async ({ id }) => { const toolName = 'get_status'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getStatus(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Priority Tools ============== server.tool( 'list_priorities', 'List all priorities', {}, async () => { const toolName = 'list_priorities'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, {}); client.setCaller(caller); try { const result = await client.listPriorities(); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_priority', 'Get details of a specific priority', { id: z.number().describe('Priority ID'), }, async ({ id }) => { const toolName = 'get_priority'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getPriority(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Time Entry Tools ============== server.tool( 'list_time_entries', 'List all time entries', { offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), sortBy: z.string().optional().describe('Sort criteria as JSON array'), }, async (params) => { const toolName = 'list_time_entries'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.listTimeEntries(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_time_entry', 'Get details of a specific time entry', { id: z.number().describe('Time entry ID'), }, async ({ id }) => { const toolName = 'get_time_entry'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getTimeEntry(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'create_time_entry', 'Create a new time entry', { projectId: z.number().describe('Project ID'), workPackageId: z.number().optional().describe('Work package ID'), activityId: z.number().describe('Activity ID'), hours: z.string().describe('Hours spent (ISO 8601 duration, e.g., PT8H30M)'), spentOn: z.string().describe('Date spent on (YYYY-MM-DD)'), comment: z.string().optional().describe('Comment for the time entry'), }, async ({ projectId, workPackageId, activityId, hours, spentOn, comment }) => { const toolName = 'create_time_entry'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { projectId, workPackageId, activityId, hours, spentOn, comment }); client.setCaller(caller); try { const data: Parameters<typeof client.createTimeEntry>[0] = { _links: { project: { href: createLink('projects', projectId) }, activity: { href: createLink('time_entries/activities', activityId) }, }, hours, spentOn, }; if (workPackageId) data._links.workPackage = { href: createLink('work_packages', workPackageId) }; if (comment) data.comment = { raw: comment }; const result = await client.createTimeEntry(data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'update_time_entry', 'Update an existing time entry', { id: z.number().describe('Time entry ID'), activityId: z.number().optional().describe('New activity ID'), hours: z.string().optional().describe('New hours'), spentOn: z.string().optional().describe('New date'), comment: z.string().optional().describe('New comment'), }, async ({ id, activityId, hours, spentOn, comment }) => { const toolName = 'update_time_entry'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id, activityId, hours, spentOn, comment }); client.setCaller(caller); try { const data: Parameters<typeof client.updateTimeEntry>[1] = { hours, spentOn, }; if (activityId) data._links = { activity: { href: createLink('time_entries/activities', activityId) } }; if (comment !== undefined) data.comment = { raw: comment }; const result = await client.updateTimeEntry(id, data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'delete_time_entry', 'Delete a time entry', { id: z.number().describe('Time entry ID'), }, async ({ id }) => { const toolName = 'delete_time_entry'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { await client.deleteTimeEntry(id); return { content: [{ type: 'text', text: `Time entry ${id} deleted successfully` }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Version Tools ============== server.tool( 'list_versions', 'List all versions/milestones', { offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), }, async (params) => { const toolName = 'list_versions'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.listVersions(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'get_version', 'Get details of a specific version', { id: z.number().describe('Version ID'), }, async ({ id }) => { const toolName = 'get_version'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getVersion(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'list_project_versions', 'List versions in a specific project', { projectId: z.union([z.number(), z.string()]).describe('Project ID or identifier'), }, async ({ projectId }) => { const toolName = 'list_project_versions'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { projectId }); client.setCaller(caller); try { const result = await client.listProjectVersions(projectId); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'create_version', 'Create a new version/milestone', { name: z.string().describe('Version name'), projectId: z.number().describe('Defining project ID'), description: z.string().optional().describe('Version description'), startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), endDate: z.string().optional().describe('End date (YYYY-MM-DD)'), status: z.enum(['open', 'locked', 'closed']).optional().describe('Version status'), sharing: z.enum(['none', 'descendants', 'hierarchy', 'tree', 'system']).optional().describe('Sharing scope'), }, async ({ name, projectId, description, startDate, endDate, status, sharing }) => { const toolName = 'create_version'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { name, projectId, description, startDate, endDate, status, sharing }); client.setCaller(caller); try { const data: Parameters<typeof client.createVersion>[0] = { name, _links: { definingProject: { href: createLink('projects', projectId) }, }, startDate, endDate, status, sharing, }; if (description) data.description = { raw: description }; const result = await client.createVersion(data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'update_version', 'Update an existing version', { id: z.number().describe('Version ID'), name: z.string().optional().describe('New version name'), description: z.string().optional().describe('New description'), startDate: z.string().optional().describe('New start date'), endDate: z.string().optional().describe('New end date'), status: z.enum(['open', 'locked', 'closed']).optional().describe('New status'), sharing: z.enum(['none', 'descendants', 'hierarchy', 'tree', 'system']).optional().describe('New sharing scope'), }, async ({ id, name, description, startDate, endDate, status, sharing }) => { const toolName = 'update_version'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id, name, description, startDate, endDate, status, sharing }); client.setCaller(caller); try { const data: Parameters<typeof client.updateVersion>[1] = { name, startDate, endDate, status, sharing, }; if (description !== undefined) data.description = { raw: description }; const result = await client.updateVersion(id, data); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( 'delete_version', 'Delete a version', { id: z.number().describe('Version ID'), }, async ({ id }) => { const toolName = 'delete_version'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { await client.deleteVersion(id); return { content: [{ type: 'text', text: `Version ${id} deleted successfully` }] }; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Activity Tools ============== server.tool( 'get_activity', 'Get details of a specific activity/journal entry', { id: z.number().describe('Activity ID'), }, async ({ id }) => { const toolName = 'get_activity'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, { id }); client.setCaller(caller); try { const result = await client.getActivity(id); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // ============== Principal Tools ============== server.tool( 'list_principals', 'List all principals (users, groups, placeholder users)', { offset: z.number().optional().describe('Page offset for pagination'), pageSize: z.number().optional().describe('Number of items per page'), filters: z.string().optional().describe('JSON filter expression'), }, async (params) => { const toolName = 'list_principals'; const caller = `tool:${toolName}`; logger.logToolInvocation(caller, toolName, params); client.setCaller(caller); try { const result = await client.listPrincipals(params); const response = { content: [{ type: 'text', text: formatResponse(result) }] }; logger.logToolResult(caller, toolName, true, result); return response; } catch (error) { logger.logToolResult(caller, toolName, false, undefined, error as Error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); return { server, initClient }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/liratanak/openproject-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server