UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

1,007 lines (954 loc) 25.5 kB
/** * Companies management tools */ import { withContext, formatResponse } from '../utils/tool-wrapper.js'; import { normalizeListParams, normalizeGetParams, filterByDetailLevel, filterArrayByDetailLevel, isEnterpriseError, } from '../utils/parameter-utils.js'; import { StandardListParams, StandardGetParams, } from '../types/parameter-types.js'; import { ProductboardError } from '../errors/index.js'; import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { fetchAllPages } from '../utils/pagination-handler.js'; export function setupCompaniesTools() { return [ { name: 'create_company', description: 'Create a new company', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Company name', }, domain: { type: 'string', description: 'Company domain', }, description: { type: 'string', description: 'Company description', }, externalId: { type: 'string', description: 'External ID for the company', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['name'], }, }, { name: 'get_companies', description: 'List all companies', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of records to return (1-100)', default: 100, minimum: 1, maximum: 100, }, startWith: { type: 'number', description: 'Number of records to skip', default: 0, minimum: 0, }, detail: { type: 'string', description: 'Level of detail (basic, standard, full)', default: 'basic', enum: ['basic', 'standard', 'full'], }, includeSubData: { type: 'boolean', description: 'Include nested sub-data', default: false, }, featureId: { type: 'string', description: 'Filter by feature ID', }, hasNotes: { type: 'boolean', description: 'Filter companies that have notes', }, term: { type: 'string', description: 'Search term', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, }, }, { name: 'get_company', description: 'Retrieve a specific company', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Company ID', }, detail: { type: 'string', description: 'Level of detail (basic, standard, full)', default: 'standard', enum: ['basic', 'standard', 'full'], }, includeSubData: { type: 'boolean', description: 'Include nested sub-data', default: false, }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id'], }, }, { name: 'update_company', description: 'Update a company', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Company ID', }, body: { type: 'object', description: 'Company data to update', properties: { name: { type: 'string', description: 'Company name', }, domain: { type: 'string', description: 'Company domain', }, description: { type: 'string', description: 'Company description', }, }, }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id', 'body'], }, }, { name: 'delete_company', description: 'Delete a company', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Company ID', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id'], }, }, // Company Field Management Tools { name: 'create_company_field', description: 'Create a new custom field for companies', inputSchema: { type: 'object', properties: { body: { type: 'object', description: 'Company field data', properties: { name: { type: 'string', description: 'Field name (max 255 characters)', maxLength: 255, }, type: { type: 'string', description: 'Field type', enum: ['text', 'number'], }, }, required: ['name', 'type'], }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['body'], }, }, { name: 'list_company_fields', description: 'List all custom fields for companies', inputSchema: { type: 'object', properties: { instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, }, }, { name: 'get_company_field', description: 'Retrieve a specific company custom field', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Company field ID', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id'], }, }, { name: 'update_company_field', description: 'Update a company custom field', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Company field ID', }, body: { type: 'object', description: 'Company field data to update', properties: { name: { type: 'string', description: 'Field name (max 255 characters)', maxLength: 255, }, type: { type: 'string', description: 'Field type', enum: ['text', 'number'], }, }, }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id', 'body'], }, }, { name: 'delete_company_field', description: 'Delete a company custom field', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Company field ID', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id'], }, }, { name: 'get_company_field_value', description: 'Get the value of a custom field for a specific company', inputSchema: { type: 'object', properties: { companyId: { type: 'string', description: 'Company ID', }, companyCustomFieldId: { type: 'string', description: 'Company custom field ID', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['companyId', 'companyCustomFieldId'], }, }, { name: 'set_company_field_value', description: 'Set the value of a custom field for a specific company', inputSchema: { type: 'object', properties: { companyId: { type: 'string', description: 'Company ID', }, companyCustomFieldId: { type: 'string', description: 'Company custom field ID', }, body: { type: 'object', description: 'Field value', properties: { value: { type: ['string', 'number'], description: 'The value to set (string for text fields, number for number fields)', }, }, required: ['value'], }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['companyId', 'companyCustomFieldId', 'body'], }, }, { name: 'delete_company_field_value', description: 'Delete the value of a custom field for a specific company', inputSchema: { type: 'object', properties: { companyId: { type: 'string', description: 'Company ID', }, companyCustomFieldId: { type: 'string', description: 'Company custom field ID', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['companyId', 'companyCustomFieldId'], }, }, ]; } export async function handleCompaniesTool(name: string, args: any) { try { switch (name) { case 'create_company': return await createCompany(args); case 'get_companies': return await listCompanies(args); case 'get_company': return await getCompany(args); case 'update_company': return await updateCompany(args); case 'delete_company': return await deleteCompany(args); // Company Field Management case 'create_company_field': return await createCompanyField(args); case 'list_company_fields': return await listCompanyFields(args); case 'get_company_field': return await getCompanyField(args); case 'update_company_field': return await updateCompanyField(args); case 'delete_company_field': return await deleteCompanyField(args); case 'get_company_field_value': return await getCompanyFieldValue(args); case 'set_company_field_value': return await setCompanyFieldValue(args); case 'delete_company_field_value': return await deleteCompanyFieldValue(args); // User management tools case 'get_users': return await getUsers(args); case 'create_user': return await createUser(args); case 'get_user': return await getUser(args); case 'update_user': return await updateUser(args); case 'delete_user': return await deleteUser(args); default: throw new Error(`Unknown companies tool: ${name}`); } } catch (error: any) { const enterpriseInfo = isEnterpriseError(error); if (enterpriseInfo.isEnterpriseFeature) { throw new ProductboardError( ErrorCode.InvalidRequest, enterpriseInfo.message, error ); } throw error; } } async function createCompany(args: any) { return await withContext( async context => { const body: any = { name: args.name, }; if (args.domain) body.domain = args.domain; if (args.description) body.description = args.description; if (args.externalId) body.externalId = args.externalId; const response = await context.axios.post( '/companies', { data: body }, { headers: { 'Productboard-Partner-Id': args['Productboard-Partner-Id'], }, } ); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function listCompanies(args: StandardListParams & any) { return await withContext( async context => { const normalized = normalizeListParams(args); const params: any = {}; // Add filters (no pagination parameters - handled by fetchAllPages) if (args.featureId) params.featureId = args.featureId; if (args.hasNotes !== undefined) params.hasNotes = args.hasNotes; if (args.term) params.term = args.term; // Use proper pagination handler to fetch all pages const paginatedResponse = await fetchAllPages( context.axios, '/companies', params, { maxItems: normalized.limit > 100 ? normalized.limit : undefined, onPageFetched: (_pageData, _pageNum, _totalSoFar) => { // Progress tracking for paginated companies fetching }, } ); const result = filterArrayByDetailLevel( paginatedResponse.data, 'company', normalized.detail ); // Remove sub-data if not requested const processedData = !normalized.includeSubData ? result.map((item: any) => { const filtered = { ...item }; // Remove complex nested objects delete filtered._embedded; delete filtered._links; return filtered; }) : result; // Return complete response structure with proper pagination info const responseData = { data: processedData, meta: { totalRecords: processedData.length, totalPages: paginatedResponse.meta?.totalPages || 1, wasLimited: paginatedResponse.meta?.wasLimited || false, }, links: {}, // Links are no longer relevant since we fetched all pages }; return { content: [ { type: 'text', text: formatResponse(responseData), }, ], }; }, args.instance, args.workspaceId ); } async function getCompany(args: StandardGetParams & { id: string } & any) { return await withContext( async context => { const normalized = normalizeGetParams(args); const response = await context.axios.get(`/companies/${args.id}`); const data = response.data; // Apply detail level filtering if (data.data) { data.data = filterByDetailLevel( data.data, 'company', normalized.detail ); } // Remove sub-data if not requested if (!normalized.includeSubData && data.data) { const filtered = { ...data.data }; delete filtered._embedded; delete filtered._links; data.data = filtered; } return { content: [ { type: 'text', text: formatResponse(data), }, ], }; }, args.instance, args.workspaceId ); } async function updateCompany(args: any) { return await withContext( async context => { // Handle case where body might be passed as a JSON string let body = args.body; if (typeof body === 'string') { try { body = JSON.parse(body); } catch { throw new Error('Invalid JSON in body parameter'); } } const response = await context.axios.patch(`/companies/${args.id}`, { data: body, }); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function deleteCompany(args: any) { return await withContext( async context => { await context.axios.delete(`/companies/${args.id}`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Company ${args.id} deleted successfully`, }), }, ], }; }, args.instance, args.workspaceId ); } // Company Field Management Functions async function createCompanyField(args: any) { return await withContext( async context => { // Handle case where body might be passed as a JSON string let body = args.body; if (typeof body === 'string') { try { body = JSON.parse(body); } catch { throw new Error('Invalid JSON in body parameter'); } } const response = await context.axios.post('/companies/custom-fields', { data: body, }); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function listCompanyFields(args: any) { return await withContext( async context => { const response = await context.axios.get('/companies/custom-fields'); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function getCompanyField(args: any) { return await withContext( async context => { const response = await context.axios.get( `/companies/custom-fields/${args.id}` ); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function updateCompanyField(args: any) { return await withContext( async context => { // Handle case where body might be passed as a JSON string let body = args.body; if (typeof body === 'string') { try { body = JSON.parse(body); } catch { throw new Error('Invalid JSON in body parameter'); } } const response = await context.axios.patch( `/companies/custom-fields/${args.id}`, { data: body, } ); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function deleteCompanyField(args: any) { return await withContext( async context => { await context.axios.delete(`/companies/custom-fields/${args.id}`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Company field ${args.id} deleted successfully`, }), }, ], }; }, args.instance, args.workspaceId ); } async function getCompanyFieldValue(args: any) { return await withContext( async context => { const response = await context.axios.get( `/companies/${args.companyId}/custom-fields/${args.companyCustomFieldId}/value` ); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function setCompanyFieldValue(args: any) { return await withContext( async context => { // Handle case where body might be passed as a JSON string let body = args.body; if (typeof body === 'string') { try { body = JSON.parse(body); } catch { throw new Error('Invalid JSON in body parameter'); } } const response = await context.axios.put( `/companies/${args.companyId}/custom-fields/${args.companyCustomFieldId}/value`, { data: body, } ); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function deleteCompanyFieldValue(args: any) { return await withContext( async context => { await context.axios.delete( `/companies/${args.companyId}/custom-fields/${args.companyCustomFieldId}/value` ); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Company field value deleted successfully`, }), }, ], }; }, args.instance, args.workspaceId ); } // User management functions async function getUsers(args: any) { return await withContext( async context => { const response = await context.axios.get('/users'); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function createUser(args: any) { return await withContext( async context => { // Handle case where body might be passed as a JSON string let body = args.body; if (typeof body === 'string') { try { body = JSON.parse(body); } catch { throw new Error('Invalid JSON in body parameter'); } } const response = await context.axios.post('/users', { data: body }); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function getUser(args: any) { return await withContext( async context => { const response = await context.axios.get(`/users/${args.id}`); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function updateUser(args: any) { return await withContext( async context => { // Handle case where body might be passed as a JSON string let body = args.body; if (typeof body === 'string') { try { body = JSON.parse(body); } catch { throw new Error('Invalid JSON in body parameter'); } } const response = await context.axios.put(`/users/${args.id}`, { data: body, }); return { content: [ { type: 'text', text: formatResponse(response.data), }, ], }; }, args.instance, args.workspaceId ); } async function deleteUser(args: any) { return await withContext( async context => { await context.axios.delete(`/users/${args.id}`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `User ${args.id} deleted successfully`, }), }, ], }; }, args.instance, args.workspaceId ); }