@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
316 lines (298 loc) • 7.86 kB
text/typescript
/**
* Products management tools
*/
import {
normalizeListParams,
normalizeGetParams,
filterArrayByDetailLevel,
filterByDetailLevel,
isEnterpriseError,
} from '../utils/parameter-utils.js';
import { withContext, formatResponse } from '../utils/tool-wrapper.js';
import { fetchAllPages } from '../utils/pagination-handler.js';
import { ProductboardError } from '../errors/index.js';
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
/**
* Products Tools
*/
export function setupProductsTools() {
return [
{
name: 'get_products',
description: 'List all products in Productboard',
inputSchema: {
type: 'object',
properties: {
...getStandardListProperties(),
...getFieldSelectionProperties(),
},
},
},
{
name: 'get_product',
description: 'Get a specific product by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Product ID',
},
...getStandardGetProperties(),
...getFieldSelectionProperties(),
},
required: ['id'],
},
},
{
name: 'update_product',
description: 'Update a product',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Product ID',
},
name: {
type: 'string',
description: 'Product name',
},
description: {
type: 'string',
description:
'Product 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)',
},
};
}
function getFieldSelectionProperties() {
return {
fields: {
type: 'array',
items: { type: 'string' },
description:
'Specific fields to include (dot notation supported for nested fields, e.g., "owner.email")',
},
exclude: {
type: 'array',
items: { type: 'string' },
description: 'Fields to exclude from response',
},
validateFields: {
type: 'boolean',
description:
'Validate field names and return suggestions for invalid fields',
},
};
}
export async function handleProductsTool(name: string, args: any) {
try {
switch (name) {
case 'get_products':
return await listProducts(args);
case 'get_product':
return await getProduct(args);
case 'update_product':
return await updateProduct(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;
}
}
/**
* List products in Productboard
*/
async function listProducts(args: any) {
return await withContext(
async context => {
const normalized = normalizeListParams(args);
const params: any = {};
// Use proper pagination handler to fetch all pages
const paginatedResponse = await fetchAllPages(
context.axios,
'/products',
params,
{
maxItems: normalized.limit > 100 ? normalized.limit : undefined,
onPageFetched: (_pageData, _pageNum, _totalSoFar) => {
// Progress tracking for paginated products fetching
},
}
);
const result = {
data: paginatedResponse.data,
links: paginatedResponse.links,
meta: {
...paginatedResponse.meta,
totalFetched: paginatedResponse.data.length,
},
};
// Apply detail level filtering after fetching all data
if (!normalized.includeSubData && result.data) {
result.data = filterArrayByDetailLevel(
result.data,
'product',
normalized.detail
);
}
// Apply client-side limit after filtering (if requested limit < total available)
if (normalized.limit && normalized.limit < result.data.length) {
result.data = result.data.slice(
normalized.startWith || 0,
(normalized.startWith || 0) + normalized.limit
);
}
return {
content: [
{
type: 'text',
text: formatResponse(result),
},
],
};
},
args.instance,
args.workspaceId,
'get_products'
);
}
/**
* Get a specific product by ID
*/
async function getProduct(args: any) {
return await withContext(
async context => {
const normalizedParams = normalizeGetParams(args);
const response = await context.axios.get(`/products/${args.id}`);
const result = filterByDetailLevel(
response.data,
'product',
normalizedParams.detail
);
return {
content: [
{
type: 'text',
text: formatResponse(result),
},
],
};
},
args.instance,
args.workspaceId,
'get_product'
);
}
/**
* Update a product
*/
async function updateProduct(args: any) {
return await withContext(
async context => {
const { id, ...updateData } = args;
const response = await context.axios.patch(`/products/${id}`, updateData);
return {
content: [
{
type: 'text',
text: formatResponse({
success: true,
product: response.data,
}),
},
],
};
},
args.instance,
args.workspaceId,
'update_product'
);
}