UNPKG

@the_cfdude/productboard-mcp

Version:

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

426 lines (397 loc) 11.9 kB
/** * Components management tools */ import { normalizeListParams, normalizeGetParams, filterArrayByDetailLevel, filterByDetailLevel, isEnterpriseError, } from '../utils/parameter-utils.js'; import { withContext, formatResponse } from '../utils/tool-wrapper.js'; import { ProductboardError, ValidationError } from '../errors/index.js'; import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; /** * Components Tools */ export function setupComponentsTools() { return [ { name: 'create_component', description: 'Create a new component in Productboard', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Component name', }, description: { type: 'string', description: 'Component description (HTML format required, e.g., "<p>Description text</p>")', }, parent: { type: 'object', description: 'Parent entity to associate this component with', properties: { product: { type: 'object', properties: { id: { type: 'string', description: 'ID of the parent product', }, }, required: ['id'], description: 'Parent product information', }, }, required: ['product'], }, ownerEmail: { type: 'string', description: 'Owner email for the component (optional)', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['name'], additionalProperties: true, }, }, { name: 'get_components', description: 'List all components in Productboard', inputSchema: { type: 'object', properties: { ...getStandardListProperties(), productId: { type: 'string', description: 'Filter by product ID', }, }, }, }, { name: 'get_component', description: 'Get a specific component by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Component ID', }, ...getStandardGetProperties(), }, required: ['id'], }, }, { name: 'update_component', description: 'Update a component', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Component ID', }, name: { type: 'string', description: 'Component name', }, description: { type: 'string', description: 'Component description (HTML format required, e.g., "<p>Description text</p>")', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id'], }, }, ]; } function getStandardListProperties() { return { limit: { type: 'number', description: 'Maximum number of items to return (1-100, default: 100)', }, startWith: { type: 'number', description: 'Offset for pagination (default: 0)', }, detail: { type: 'string', enum: ['basic', 'standard', 'full'], description: 'Level of detail (default: basic). DEPRECATED: Use fields parameter for precise selection.', }, outputFormat: { type: 'string', enum: ['json', 'markdown', 'csv', 'summary'], description: 'Output format for response data. JSON (default), Markdown (human-readable), CSV (tabular), Summary (condensed)', }, includeSubData: { type: 'boolean', description: 'Include nested complex JSON sub-data', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }; } function getStandardGetProperties() { return { detail: { type: 'string', enum: ['basic', 'standard', 'full'], description: 'Level of detail (default: standard). DEPRECATED: Use fields parameter for precise selection.', }, outputFormat: { type: 'string', enum: ['json', 'markdown', 'csv', 'summary'], description: 'Output format for response data. JSON (default), Markdown (human-readable), CSV (tabular), Summary (condensed)', }, includeSubData: { type: 'boolean', description: 'Include nested complex JSON sub-data', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }; } export async function handleComponentsTool(name: string, args: any) { try { switch (name) { case 'create_component': return await createComponent(args); case 'get_components': return await listComponents(args); case 'get_component': return await getComponent(args); case 'update_component': return await updateComponent(args); default: throw new ProductboardError( ErrorCode.InvalidRequest, `Unknown tool: ${name}` ); } } catch (error: any) { if (isEnterpriseError(error)) { throw new ProductboardError( ErrorCode.InvalidRequest, error.message, error ); } throw error; } } /** * Create a new component in Productboard */ async function createComponent(args: any) { return await withContext( async context => { try { // Validate required fields with helpful error messages if (!args.name) { throw new ValidationError( 'Component name is required. Example: { "name": "Frontend UI", "description": "<p>React components with <b>modern</b> design</p>", "parent": { "product": { "id": "12345" } } }', 'name' ); } const body: any = { name: args.name, }; if (args.description) body.description = args.description; if (args.parent) body.parent = args.parent; // Add owner email if provided (as shown in API example) if (args.ownerEmail) { body.owner = { email: args.ownerEmail }; } const response = await context.axios.post('/components', { data: body, }); return { content: [ { type: 'text', text: formatResponse({ success: true, component: response.data, }), }, ], }; } catch (error: any) { // Enhanced error handling for HTML validation errors if ( error.response?.data && typeof error.response.data === 'string' && error.response.data.includes('cvc-complex-type') ) { throw new ValidationError( `HTML validation failed in description field. Productboard only allows these exact HTML tags: <b>, <i>, <s>, <u>, <br>, <a>, <code>, <img>. Replace <strong> with <b>, <em> with <i>, etc. Example: "<p>Component with <b>bold text</b> and <i>italic text</i></p>"`, 'html_tags' ); } // Enhanced error handling for common 404 scenarios if (error.response?.status === 404) { throw new ValidationError( `Component creation failed - endpoint not found. Ensure you're using the correct format: { "name": "Component Name", "parent": { "product": { "id": "valid-product-id" } } }. Use 'get_products()' to find valid product IDs.`, 'request_format' ); } if (error.response?.status === 400) { const apiError = error.response.data?.message || 'Invalid request format'; // Check if it's an HTML validation error specifically if ( apiError.includes('cvc-complex-type') || apiError.includes('Invalid content was found') ) { throw new ValidationError( `HTML validation error: ${apiError}. Productboard only allows these HTML tags: <b>, <i>, <s>, <u>, <br>, <a>, <code>, <img>. Avoid <strong>, <em>, <div>, <span>, etc. Use: "<p>Text with <b>bold</b> and <i>italic</i> formatting</p>"`, 'html_validation' ); } throw new ValidationError( `${apiError}. Required format: { "name": "string", "parent": { "product": { "id": "string" } } (optional), "description": "string with allowed HTML tags: <b>, <i>, <s>, <u>, <br>, <a>, <code>, <img>" (optional) }`, 'request_body' ); } throw error; } }, args.instance, args.workspaceId, 'create_component' ); } /** * List components in Productboard */ async function listComponents(args: any) { return await withContext( async context => { const normalizedParams = normalizeListParams(args); const params: any = {}; // Remove problematic pagination parameters that cause API errors // ProductBoard API doesn't accept 'limit', 'pageLimit', or 'pageOffset' // Note: The Productboard API /components endpoint doesn't support filtering // We'll need to fetch all and filter client-side // if (args.productId) params['parent.id'] = args.productId; const response = await context.axios.get('/components', { params }); // Return the full response object with data array, matching companies pattern const data = response.data; // Apply detail level filtering if needed if (data.data && Array.isArray(data.data)) { data.data = filterArrayByDetailLevel( data.data, 'component', normalizedParams.detail ); } return { content: [ { type: 'text', text: formatResponse(data), }, ], }; }, args.instance, args.workspaceId, 'get_components' ); } /** * Get a specific component by ID */ async function getComponent(args: any) { return await withContext( async context => { const normalizedParams = normalizeGetParams(args); const response = await context.axios.get(`/components/${args.id}`); const result = filterByDetailLevel( response.data, 'component', normalizedParams.detail ); return { content: [ { type: 'text', text: formatResponse(result), }, ], }; }, args.instance, args.workspaceId, 'get_component' ); } /** * Update a component */ async function updateComponent(args: any) { return await withContext( async context => { const { id, ...updateData } = args; const response = await context.axios.patch( `/components/${id}`, updateData ); return { content: [ { type: 'text', text: formatResponse({ success: true, component: response.data, }), }, ], }; }, args.instance, args.workspaceId, 'update_component' ); }