@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
327 lines (326 loc) • 11.7 kB
JavaScript
/**
* Users management tools
*/
import { withContext, formatResponse } from '../utils/tool-wrapper.js';
import { normalizeListParams, normalizeGetParams, filterByDetailLevel, filterArrayByDetailLevel, isEnterpriseError, } from '../utils/parameter-utils.js';
import { fetchAllPages } from '../utils/pagination-handler.js';
import { ProductboardError } from '../errors/index.js';
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
export function setupUsersTools() {
return [
{
name: 'create_user',
description: 'Create a new user in Productboard',
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'User email address',
},
name: {
type: 'string',
description: 'User full name',
},
role: {
type: 'string',
description: 'User role (e.g., admin, member, viewer)',
},
companyId: {
type: 'string',
description: 'Company ID to associate with the user',
},
externalId: {
type: 'string',
description: 'External ID for the user',
},
instance: {
type: 'string',
description: 'Productboard instance name (optional)',
},
workspaceId: {
type: 'string',
description: 'Workspace ID (optional)',
},
},
required: ['email'],
},
},
{
name: 'get_users',
description: 'List all users in Productboard',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of users 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_user',
description: 'Get a specific user by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'User 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: 'update_user',
description: 'Update an existing user',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'User ID',
},
name: {
type: 'string',
description: 'Updated user name',
},
role: {
type: 'string',
description: 'Updated user role',
},
companyId: {
type: 'string',
description: 'Updated company ID',
},
externalId: {
type: 'string',
description: 'Updated external ID',
},
instance: {
type: 'string',
description: 'Productboard instance name (optional)',
},
workspaceId: {
type: 'string',
description: 'Workspace ID (optional)',
},
},
required: ['id'],
},
},
{
name: 'delete_user',
description: 'Delete a user',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'User ID',
},
instance: {
type: 'string',
description: 'Productboard instance name (optional)',
},
workspaceId: {
type: 'string',
description: 'Workspace ID (optional)',
},
},
required: ['id'],
},
},
];
}
export async function handleUsersTool(name, args) {
try {
switch (name) {
case 'create_user':
return await createUser(args);
case 'get_users':
return await listUsers(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 users tool: ${name}`);
}
}
catch (error) {
const enterpriseInfo = isEnterpriseError(error);
if (enterpriseInfo.isEnterpriseFeature) {
throw new ProductboardError(ErrorCode.InvalidRequest, enterpriseInfo.message, error);
}
throw error;
}
}
async function createUser(args) {
return await withContext(async (context) => {
const body = {
email: args.email,
};
if (args.name)
body.name = args.name;
if (args.role)
body.role = args.role;
if (args.companyId)
body.company = { id: args.companyId };
if (args.externalId)
body.externalId = args.externalId;
const response = await context.axios.post('/users', body);
return {
content: [
{
type: 'text',
text: formatResponse({
success: true,
user: response.data,
}),
},
],
};
}, args.instance, args.workspaceId);
}
async function listUsers(args) {
return await withContext(async (context) => {
const normalized = normalizeListParams(args);
const params = {};
// Use proper pagination handler to fetch all pages
const paginatedResponse = await fetchAllPages(context.axios, '/users', params, {
maxItems: normalized.limit > 100 ? normalized.limit : undefined,
onPageFetched: (_pageData, _pageNum, _totalSoFar) => {
// Progress tracking for paginated users 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, 'user', 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);
}
async function getUser(args) {
return await withContext(async (context) => {
const normalizedParams = normalizeGetParams(args);
const response = await context.axios.get(`/users/${args.id}`);
let result = response.data;
// Apply detail level filtering
if (!normalizedParams.includeSubData) {
result = filterByDetailLevel(result, 'user', normalizedParams.detail);
}
return {
content: [
{
type: 'text',
text: formatResponse(result),
},
],
};
}, args.instance, args.workspaceId);
}
async function updateUser(args) {
return await withContext(async (context) => {
const body = {};
if (args.name)
body.name = args.name;
if (args.role)
body.role = args.role;
if (args.companyId)
body.company = { id: args.companyId };
if (args.externalId)
body.externalId = args.externalId;
const response = await context.axios.patch(`/users/${args.id}`, {
data: body,
});
return {
content: [
{
type: 'text',
text: formatResponse({
success: true,
user: response.data,
}),
},
],
};
}, args.instance, args.workspaceId);
}
async function deleteUser(args) {
return await withContext(async (context) => {
await context.axios.delete(`/users/${args.id}`);
return {
content: [
{
type: 'text',
text: formatResponse({
success: true,
message: `User ${args.id} deleted successfully`,
}),
},
],
};
}, args.instance, args.workspaceId);
}