UNPKG

@the_cfdude/productboard-mcp

Version:

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

267 lines (266 loc) 10.2 kB
/** * Webhooks management tools */ import { withContext, formatResponse } from '../utils/tool-wrapper.js'; import { normalizeListParams, normalizeGetParams, filterByDetailLevel, filterArrayByDetailLevel, isEnterpriseError, } from '../utils/parameter-utils.js'; import { ProductboardError } from '../errors/index.js'; import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; export function setupWebhooksTools() { return [ { name: 'create_webhook', description: 'Create a new webhook subscription', inputSchema: { type: 'object', properties: { events: { type: 'array', description: 'Array of event types to subscribe to (e.g., [{eventType: "feature.created"}, {eventType: "note.updated"}])', items: { type: 'object', properties: { eventType: { type: 'string', description: 'Event type (e.g., feature.created, note.updated, objective.created)', }, }, required: ['eventType'], }, }, name: { type: 'string', description: 'Name for the webhook subscription', }, url: { type: 'string', description: 'Webhook URL to receive notifications', }, headers: { type: 'object', description: 'Optional headers to include in webhook requests (e.g., {"X-Custom-Header": "value", "Content-Type": "application/json"})', }, version: { type: 'number', description: 'Notification version (default: 1)', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['events', 'name', 'url'], }, }, { name: 'list_webhooks', description: 'List all webhook subscriptions', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of webhooks 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)', }, 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)', }, }, }, }, { name: 'get_webhook', description: 'Get a specific webhook subscription by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Webhook ID', }, detail: { type: 'string', enum: ['basic', 'standard', 'full'], description: 'Level of detail (default: standard)', }, 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)', }, }, required: ['id'], }, }, { name: 'delete_webhook', description: 'Delete a webhook subscription', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Webhook ID', }, instance: { type: 'string', description: 'Productboard instance name (optional)', }, workspaceId: { type: 'string', description: 'Workspace ID (optional)', }, }, required: ['id'], }, }, ]; } export async function handleWebhooksTool(name, args) { try { switch (name) { case 'create_webhook': case 'post_webhook': // Support both names for compatibility return await createWebhook(args); case 'list_webhooks': case 'get_webhooks': // Support both names for compatibility return await listWebhooks(args); case 'get_webhook': return await getWebhook(args); case 'delete_webhook': return await deleteWebhook(args); default: throw new Error(`Unknown webhooks tool: ${name}`); } } catch (error) { const enterpriseInfo = isEnterpriseError(error); if (enterpriseInfo.isEnterpriseFeature) { throw new ProductboardError(ErrorCode.InvalidRequest, enterpriseInfo.message, error); } throw error; } } async function createWebhook(args) { return await withContext(async (context) => { // Handle both direct parameters and body parameter formats let webhookParams = args; if (args.body) { // Parse body if it's a string (from MCP calls) webhookParams = typeof args.body === 'string' ? JSON.parse(args.body) : args.body; } // Build the webhook data structure const webhookData = { events: webhookParams.events, name: webhookParams.name, notification: webhookParams.notification || { version: webhookParams.version || 1, url: webhookParams.url, }, }; // Add optional headers if provided if (webhookParams.headers && !webhookData.notification.headers) { webhookData.notification.headers = webhookParams.headers; } const response = await context.axios.post('/webhooks', { data: webhookData, }); return { content: [ { type: 'text', text: formatResponse({ success: true, webhook: response.data, }), }, ], }; }, args.instance, args.workspaceId); } async function listWebhooks(args) { return await withContext(async (context) => { const normalizedParams = normalizeListParams(args); const params = {}; // Remove problematic pagination parameters that cause API errors // ProductBoard API doesn't accept 'limit', 'pageLimit', or 'pageOffset' const response = await context.axios.get('/webhooks', { params }); const result = response.data; // Apply detail level filtering if (!normalizedParams.includeSubData && result.data) { result.data = filterArrayByDetailLevel(result.data, 'webhook', normalizedParams.detail); } return { content: [ { type: 'text', text: formatResponse(result), }, ], }; }, args.instance, args.workspaceId); } async function getWebhook(args) { return await withContext(async (context) => { const normalizedParams = normalizeGetParams(args); const response = await context.axios.get(`/webhooks/${args.id}`); let result = response.data; // Apply detail level filtering if (!normalizedParams.includeSubData) { result = filterByDetailLevel(result, 'webhook', normalizedParams.detail); } return { content: [ { type: 'text', text: formatResponse(result), }, ], }; }, args.instance, args.workspaceId); } async function deleteWebhook(args) { return await withContext(async (context) => { await context.axios.delete(`/webhooks/${args.id}`); return { content: [ { type: 'text', text: formatResponse({ success: true, message: `Webhook ${args.id} deleted successfully`, }), }, ], }; }, args.instance, args.workspaceId); }