@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
text/typescript
/**
* 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
);
}