bottlenecks-mcp-server
Version:
Model Context Protocol server for Bottlenecks database - enables AI agents like Claude to interact with bottleneck data
587 lines • 24.9 kB
JavaScript
/**
* MCP CRUD Operations Tools
* Provides AI agents with create, update, and validation capabilities for bottlenecks
*/
/**
* Create a new bottleneck
*/
export function createCreateBottleneckTool(client) {
return {
type: 'call_tool',
name: 'create_bottleneck',
description: 'Create a new bottleneck with title, description, and metadata',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Clear, descriptive title of the bottleneck',
minLength: 5,
maxLength: 200,
},
description: {
type: 'string',
description: 'Detailed description using MDX format with sections',
minLength: 50,
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Categorization tags for the bottleneck',
maxItems: 10,
},
is_public: {
type: 'boolean',
description: 'Whether the bottleneck should be publicly visible',
default: false,
},
organization_id: {
type: 'string',
description: 'ID of the organization this bottleneck belongs to',
format: 'uuid',
},
card_type: {
type: 'string',
description: 'Type of card generation: AI (default), HYBRID, or REAL',
enum: ['AI', 'HYBRID', 'REAL'],
default: 'AI',
},
validate_only: {
type: 'boolean',
description: 'Only validate the data without creating the bottleneck',
default: false,
},
},
required: ['title', 'description'],
},
handler: async (args) => {
try {
// Validate required permissions
const hasWritePermission = await client.checkPermissions(['write']);
if (!hasWritePermission) {
return {
content: [
{
type: 'text',
text: 'Error: Write permission required to create bottlenecks. Please check your API key scopes.',
},
],
isError: true,
};
}
// Prepare the data
const bottleneckData = {
title: args.title,
description: args.description,
tags: args.tags || [],
is_public: args.is_public || false,
organization_id: args.organization_id,
card_type: args.card_type || 'AI',
};
// If validation only, use the validation endpoint
if (args.validate_only) {
const validationResponse = await client.request('/api/mcp/validate-bottleneck', {
method: 'POST',
body: bottleneckData,
requireAuth: true,
});
if (!validationResponse.success) {
return {
content: [
{
type: 'text',
text: `Validation error: ${validationResponse.error}`,
},
],
isError: true,
};
}
const validation = validationResponse.data;
let content = `# Validation Results\n\n`;
if (validation.valid) {
content += `✅ **Valid:** The bottleneck data passes all validation checks.\n\n`;
}
else {
content += `❌ **Invalid:** The bottleneck data has validation errors.\n\n`;
content += `## Errors\n\n`;
validation.errors.forEach((error) => {
content += `- **${error.field}:** ${error.message}\n`;
});
content += '\n';
}
if (validation.warnings && validation.warnings.length > 0) {
content += `## Warnings\n\n`;
validation.warnings.forEach((warning) => {
content += `- **${warning.field}:** ${warning.message}\n`;
});
content += '\n';
}
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
// Create the bottleneck
const response = await client.request('/api/cards', {
method: 'POST',
body: bottleneckData,
requireAuth: true,
});
if (!response.success) {
return {
content: [
{
type: 'text',
text: `Error creating bottleneck: ${response.error}`,
},
],
isError: true,
};
}
const bottleneck = response.data.data;
let content = `# Bottleneck Created Successfully! 🎉\n\n`;
content += `**ID:** \`${bottleneck.id}\`\n`;
content += `**Title:** ${bottleneck.title}\n`;
content += `**Slug:** \`${bottleneck.slug}\`\n`;
content += `**Public:** ${bottleneck.is_public ? 'Yes' : 'No'}\n`;
content += `**Created:** ${new Date(bottleneck.created_at).toLocaleDateString()}\n`;
if (bottleneck.tags && bottleneck.tags.length > 0) {
content += `**Tags:** ${bottleneck.tags.map((tag) => `\`${tag}\``).join(', ')}\n`;
}
content += `\n**View Online:** [${bottleneck.title}](${client.getApiBaseUrl()}/cards/${bottleneck.slug})\n`;
content += `**Direct URL:** \`${client.getApiBaseUrl()}/cards/${bottleneck.slug}\`\n`;
content += `\n**Next steps:**\n`;
content += `- Use \`upload_file\` to add supporting documents\n`;
content += `- Use \`update_bottleneck\` to modify the content\n`;
content += `- Use \`get_bottleneck\` to view the full details\n`;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error in create_bottleneck: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
},
};
}
/**
* Update an existing bottleneck
*/
export function createUpdateBottleneckTool(client) {
return {
type: 'call_tool',
name: 'update_bottleneck',
description: 'Update an existing bottleneck with new information',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Bottleneck ID to update',
format: 'uuid',
},
title: {
type: 'string',
description: 'New title for the bottleneck',
minLength: 5,
maxLength: 200,
},
description: {
type: 'string',
description: 'New description using MDX format',
minLength: 50,
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'New tags for the bottleneck',
maxItems: 10,
},
is_public: {
type: 'boolean',
description: 'Update public visibility',
},
organization_id: {
type: 'string',
description: 'Update organization assignment',
format: 'uuid',
},
validate_only: {
type: 'boolean',
description: 'Only validate the changes without applying them',
default: false,
},
},
required: ['id'],
},
handler: async (args) => {
try {
// Validate required permissions
const hasWritePermission = await client.checkPermissions(['write']);
if (!hasWritePermission) {
return {
content: [
{
type: 'text',
text: 'Error: Write permission required to update bottlenecks. Please check your API key scopes.',
},
],
isError: true,
};
}
// Prepare the update data (only include provided fields)
const updateData = {};
if (args.title !== undefined)
updateData.title = args.title;
if (args.description !== undefined)
updateData.description = args.description;
if (args.tags !== undefined)
updateData.tags = args.tags;
if (args.is_public !== undefined)
updateData.is_public = args.is_public;
if (args.organization_id !== undefined)
updateData.organization_id = args.organization_id;
// Check if any updates were provided
if (Object.keys(updateData).length === 0) {
return {
content: [
{
type: 'text',
text: 'Error: No update fields provided. Specify at least one field to update.',
},
],
isError: true,
};
}
// If validation only, validate the changes
if (args.validate_only) {
const validationResponse = await client.request('/api/mcp/validate-bottleneck', {
method: 'POST',
body: { ...updateData, id: args.id },
requireAuth: true,
});
if (!validationResponse.success) {
return {
content: [
{
type: 'text',
text: `Validation error: ${validationResponse.error}`,
},
],
isError: true,
};
}
const validation = validationResponse.data;
let content = `# Update Validation Results\n\n`;
if (validation.valid) {
content += `✅ **Valid:** The update data passes all validation checks.\n\n`;
content += `**Fields to update:**\n`;
Object.keys(updateData).forEach((field) => {
content += `- ${field}\n`;
});
}
else {
content += `❌ **Invalid:** The update data has validation errors.\n\n`;
content += `## Errors\n\n`;
validation.errors.forEach((error) => {
content += `- **${error.field}:** ${error.message}\n`;
});
}
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
// Apply the update
const response = await client.request(`/api/cards/${args.id}`, {
method: 'PUT',
body: updateData,
requireAuth: true,
});
if (!response.success) {
return {
content: [
{
type: 'text',
text: `Error updating bottleneck: ${response.error}`,
},
],
isError: true,
};
}
const bottleneck = response.data;
let content = `# Bottleneck Updated Successfully! ✅\n\n`;
content += `**ID:** \`${bottleneck.id}\`\n`;
content += `**Title:** ${bottleneck.title}\n`;
content += `**Updated:** ${new Date(bottleneck.updated_at).toLocaleDateString()}\n`;
content += `\n**Fields updated:**\n`;
Object.keys(updateData).forEach((field) => {
content += `- ${field}\n`;
});
content += `\n**View Online:** [${bottleneck.title}](${client.getApiBaseUrl()}/cards/${bottleneck.slug})\n`;
content += `**Direct URL:** \`${client.getApiBaseUrl()}/cards/${bottleneck.slug}\`\n`;
content += `\n**Use \`get_bottleneck\` to view the updated content.**\n`;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error in update_bottleneck: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
},
};
}
/**
* Validate bottleneck data without creating/updating
*/
export function createValidateBottleneckDataTool(client) {
return {
type: 'call_tool',
name: 'validate_bottleneck_data',
description: 'Validate bottleneck data against schema and business rules without creating or updating',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Title to validate',
minLength: 1,
maxLength: 200,
},
description: {
type: 'string',
description: 'Description to validate (MDX format)',
minLength: 1,
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags to validate',
maxItems: 10,
},
check_duplicates: {
type: 'boolean',
description: 'Check for duplicate titles/slugs',
default: true,
},
check_mdx_format: {
type: 'boolean',
description: 'Validate MDX format and sections',
default: true,
},
check_taxonomy: {
type: 'boolean',
description: 'Validate tags against taxonomy',
default: true,
},
},
required: [],
},
handler: async (args) => {
try {
// Client-side validation first
const errors = [];
const warnings = [];
// Title validation
if (args.title) {
if (args.title.length < 5) {
errors.push({
field: 'title',
message: 'Title must be at least 5 characters long',
code: 'MIN_LENGTH',
});
}
if (args.title.length > 200) {
errors.push({
field: 'title',
message: 'Title must be no more than 200 characters long',
code: 'MAX_LENGTH',
});
}
if (args.title.toLowerCase().includes('bottleneck')) {
warnings.push({
field: 'title',
message: 'Consider avoiding the word "bottleneck" in the title for better specificity',
code: 'GENERIC_TERM',
});
}
}
// Description validation
if (args.description) {
if (args.description.length < 50) {
errors.push({
field: 'description',
message: 'Description must be at least 50 characters long',
code: 'MIN_LENGTH',
});
}
if (args.check_mdx_format) {
// Basic MDX validation - let the server handle detailed section validation
if (args.description.length < 100) {
warnings.push({
field: 'description',
message: 'Description seems short for a comprehensive bottleneck analysis',
code: 'SHORT_DESCRIPTION',
});
}
}
}
// Summary validation (if provided as separate field)
if (args.summary) {
if (args.summary.length < 20) {
errors.push({
field: 'summary',
message: 'Summary must be at least 20 characters long',
code: 'MIN_LENGTH',
});
}
if (args.summary.length > 500) {
errors.push({
field: 'summary',
message: 'Summary must be no more than 500 characters long',
code: 'MAX_LENGTH',
});
}
}
// Tags validation
if (args.tags) {
if (args.tags.length > 10) {
errors.push({
field: 'tags',
message: 'Maximum 10 tags allowed',
code: 'MAX_ITEMS',
});
}
args.tags.forEach((tag, index) => {
if (tag.length === 0) {
errors.push({
field: `tags[${index}]`,
message: 'Tag cannot be empty',
code: 'EMPTY_TAG',
});
}
if (tag !== tag.toLowerCase()) {
warnings.push({
field: `tags[${index}]`,
message: 'Tags should be lowercase',
code: 'CASE_CONVENTION',
});
}
if (tag.includes(' ')) {
warnings.push({
field: `tags[${index}]`,
message: 'Tags should use hyphens instead of spaces',
code: 'SPACE_IN_TAG',
});
}
});
}
// Server-side validation if we have data to validate
if (args.title || args.description || args.tags) {
const validationData = {
title: args.title,
description: args.description,
tags: args.tags,
};
const response = await client.request('/api/mcp/validate-bottleneck', {
method: 'POST',
body: validationData,
requireAuth: true,
});
if (response.success && response.data) {
// Merge server-side validation results
if (response.data.errors) {
errors.push(...response.data.errors);
}
if (response.data.warnings) {
warnings.push(...response.data.warnings);
}
}
}
// Format results
const isValid = errors.length === 0;
let content = `# Validation Results\n\n`;
if (isValid) {
content += `✅ **Valid:** All validation checks passed!\n\n`;
}
else {
content += `❌ **Invalid:** Found ${errors.length} error(s)\n\n`;
}
if (errors.length > 0) {
content += `## Errors\n\n`;
errors.forEach((error, index) => {
content += `${index + 1}. **${error.field}:** ${error.message} \`(${error.code})\`\n`;
});
content += '\n';
}
if (warnings.length > 0) {
content += `## Warnings\n\n`;
warnings.forEach((warning, index) => {
content += `${index + 1}. **${warning.field}:** ${warning.message} \`(${warning.code})\`\n`;
});
content += '\n';
}
if (isValid) {
content += `**Ready to create/update!** Use \`create_bottleneck\` or \`update_bottleneck\` with this data.\n`;
}
else {
content += `**Fix the errors above before creating/updating the bottleneck.**\n`;
}
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error in validate_bottleneck_data: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
},
};
}
//# sourceMappingURL=crud-operations.js.map