@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
393 lines (370 loc) • 9.96 kB
text/typescript
/**
* 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 {
StandardListParams,
StandardGetParams,
} from '../types/parameter-types.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: string, args: any) {
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: any) {
const enterpriseInfo = isEnterpriseError(error);
if (enterpriseInfo.isEnterpriseFeature) {
throw new ProductboardError(
ErrorCode.InvalidRequest,
enterpriseInfo.message,
error
);
}
throw error;
}
}
async function createUser(args: any) {
return await withContext(
async context => {
const body: any = {
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: StandardListParams & 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,
'/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: StandardGetParams & {
id: string;
instance?: string;
workspaceId?: string;
}
) {
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: any) {
return await withContext(
async context => {
const body: any = {};
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: any) {
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
);
}