@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
267 lines (266 loc) • 10.2 kB
JavaScript
/**
* 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);
}