bottlenecks-mcp-server
Version:
Model Context Protocol server for Bottlenecks database - enables AI agents like Claude to interact with bottleneck data
523 lines • 23 kB
JavaScript
/**
* MCP Read Operations Tools
* Provides AI agents with search, list, and get capabilities for bottlenecks
*/
/**
* Search bottlenecks with filters and pagination
*/
export function createSearchBottlenecksTool(client) {
return {
type: 'call_tool',
name: 'search_bottlenecks',
description: 'Search for bottlenecks using various filters and criteria',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Text search query (searches title and description)',
maxLength: 200,
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by tags (AND logic)',
maxItems: 10,
},
author_id: {
type: 'string',
description: 'Filter by author ID',
format: 'uuid',
},
organization_id: {
type: 'string',
description: 'Filter by organization ID',
format: 'uuid',
},
is_public: {
type: 'boolean',
description: 'Filter by public/private status',
},
approval_status: {
type: 'string',
description: 'Filter by approval status (admin only)',
enum: ['approved', 'pending', 'rejected', 'draft'],
},
include_unapproved: {
type: 'boolean',
description: 'Include unapproved cards (admin only)',
default: false,
},
created_after: {
type: 'string',
description: 'Filter by creation date (ISO 8601)',
format: 'date-time',
},
created_before: {
type: 'string',
description: 'Filter by creation date (ISO 8601)',
format: 'date-time',
},
has_attachments: {
type: 'boolean',
description: 'Filter by presence of file attachments',
},
page: {
type: 'number',
description: 'Page number (1-based)',
minimum: 1,
default: 1,
},
limit: {
type: 'number',
description: 'Number of results per page',
minimum: 1,
maximum: 100,
default: 20,
},
sort_by: {
type: 'string',
description: 'Sort field',
enum: ['created_at', 'updated_at', 'title', 'relevance'],
default: 'created_at',
},
sort_order: {
type: 'string',
description: 'Sort order',
enum: ['asc', 'desc'],
default: 'desc',
},
},
required: [],
},
handler: async (args) => {
try {
// Build query parameters
const params = new URLSearchParams();
if (args.query)
params.append('q', args.query);
if (args.tags && args.tags.length > 0) {
args.tags.forEach((tag) => params.append('tags', tag));
}
if (args.author_id)
params.append('author_id', args.author_id);
if (args.organization_id)
params.append('organization_id', args.organization_id);
if (args.is_public !== undefined)
params.append('is_public', args.is_public.toString());
if (args.created_after)
params.append('created_after', args.created_after);
if (args.created_before)
params.append('created_before', args.created_before);
if (args.has_attachments !== undefined)
params.append('has_attachments', args.has_attachments.toString());
if (args.approval_status)
params.append('status', args.approval_status);
if (args.page)
params.append('page', args.page.toString());
if (args.limit)
params.append('limit', args.limit.toString());
if (args.sort_by)
params.append('sort_by', args.sort_by);
if (args.sort_order)
params.append('sort_order', args.sort_order);
// Use admin endpoint if accessing unapproved content or specific approval status
const useAdminEndpoint = args.include_unapproved || args.approval_status;
const endpoint = useAdminEndpoint
? `/api/admin/cards?${params.toString()}`
: `/api/cards?${params.toString()}`;
const response = await client.request(endpoint, {
method: 'GET',
requireAuth: true,
});
if (!response.success) {
return {
content: [
{
type: 'text',
text: `Error searching bottlenecks: ${response.error}`,
},
],
isError: true,
};
}
const data = response.data;
let content = `# Search Results\n\n`;
// Handle both regular API response (data.cards) and admin API response (data array or data.data)
const cards = data.cards || data.data || data || [];
if (!Array.isArray(cards) || cards.length === 0) {
content += `No bottlenecks found matching your criteria.\n\n`;
content += `**Search parameters:**\n`;
if (args.query)
content += `- Query: "${args.query}"\n`;
if (args.tags)
content += `- Tags: ${args.tags.join(', ')}\n`;
if (args.is_public !== undefined)
content += `- Public: ${args.is_public}\n`;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
const total = data.total || data.pagination?.total || cards.length;
const currentPage = data.page || data.pagination?.page || args.page || 1;
content += `Found **${total}** bottlenecks (showing ${cards.length} on page ${currentPage})\n\n`;
cards.forEach((bottleneck, index) => {
content += `## ${index + 1}. [${bottleneck.title}](${client.getApiBaseUrl()}/cards/${bottleneck.slug})\n`;
content += `**ID:** \`${bottleneck.id}\`\n`;
content += `**Slug:** \`${bottleneck.slug}\`\n`;
content += `**Created:** ${new Date(bottleneck.created_at).toLocaleDateString()}\n`;
content += `**Status:** ${bottleneck.status || 'unknown'}\n`;
content += `**Public:** ${bottleneck.is_public ? 'Yes' : 'No'}\n`;
if (bottleneck.tags && bottleneck.tags.length > 0) {
content += `**Tags:** ${bottleneck.tags.map((tag) => `\`${tag}\``).join(', ')}\n`;
}
if (bottleneck.file_attachments &&
bottleneck.file_attachments.length > 0) {
content += `**Attachments:** ${bottleneck.file_attachments.length} files\n`;
}
// Show description preview (first 200 chars)
if (bottleneck.description) {
const preview = bottleneck.description.length > 200
? bottleneck.description.substring(0, 200) + '...'
: bottleneck.description;
content += `**Preview:** ${preview.replace(/\n/g, ' ')}\n`;
}
content += '\n';
});
// Pagination info
if (data.hasMore) {
content += `**Next page:** Use \`page: ${data.page + 1}\` to see more results\n`;
}
content += `\n**Use \`get_bottleneck\` with an ID to see full details.**\n`;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error in search_bottlenecks: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
},
};
}
/**
* Get a specific bottleneck by ID
*/
export function createGetBottleneckTool(client) {
return {
type: 'call_tool',
name: 'get_bottleneck',
description: 'Get detailed information about a specific bottleneck by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Bottleneck ID (UUID)',
format: 'uuid',
},
include_attachments: {
type: 'boolean',
description: 'Include file attachment details',
default: true,
},
format: {
type: 'string',
description: 'Response format',
enum: ['markdown', 'json', 'raw'],
default: 'markdown',
},
},
required: ['id'],
},
handler: async (args) => {
try {
const params = new URLSearchParams();
if (args.include_attachments)
params.append('include_attachments', 'true');
const response = await client.request(`/api/cards/${args.id}?${params.toString()}`, {
method: 'GET',
requireAuth: true,
});
if (!response.success) {
return {
content: [
{
type: 'text',
text: `Error getting bottleneck: ${response.error}`,
},
],
isError: true,
};
}
const bottleneck = response.data;
if (args.format === 'json') {
return {
content: [
{
type: 'text',
text: JSON.stringify(bottleneck, null, 2),
},
],
};
}
if (args.format === 'raw') {
return {
content: [
{
type: 'text',
text: bottleneck.description || 'No description available',
},
],
};
}
// Markdown format (default)
let content = `# ${bottleneck.title}\n\n`;
content += `**ID:** \`${bottleneck.id}\`\n`;
content += `**Slug:** \`${bottleneck.slug}\`\n`;
content += `**Created:** ${new Date(bottleneck.created_at).toLocaleDateString()}\n`;
content += `**Updated:** ${new Date(bottleneck.updated_at).toLocaleDateString()}\n`;
content += `**Public:** ${bottleneck.is_public ? 'Yes' : 'No'}\n`;
if (bottleneck.tags && bottleneck.tags.length > 0) {
content += `**Tags:** ${bottleneck.tags.map((tag) => `\`${tag}\``).join(', ')}\n`;
}
if (bottleneck.author_id) {
content += `**Author ID:** \`${bottleneck.author_id}\`\n`;
}
if (bottleneck.organization_id) {
content += `**Organization ID:** \`${bottleneck.organization_id}\`\n`;
}
content += `\n**View Online:** [${bottleneck.title}](${client.getApiBaseUrl()}/cards/${bottleneck.slug})\n`;
content += `**Direct URL:** \`${client.getApiBaseUrl()}/cards/${bottleneck.slug}\`\n`;
content += '\n---\n\n';
// Add the full description
if (bottleneck.description) {
content += bottleneck.description;
}
else {
content += '*No description available*';
}
// Add file attachments if present
if (args.include_attachments &&
bottleneck.file_attachments &&
bottleneck.file_attachments.length > 0) {
content += '\n\n---\n\n## File Attachments\n\n';
bottleneck.file_attachments.forEach((file, index) => {
content += `### ${index + 1}. ${file.original_filename}\n`;
content += `- **ID:** \`${file.id}\`\n`;
content += `- **Type:** ${file.upload_type}\n`;
content += `- **Size:** ${(file.file_size / 1024 / 1024).toFixed(2)} MB\n`;
content += `- **Format:** ${file.mime_type}\n`;
content += `- **Status:** ${file.processing_status}\n`;
content += `- **Public:** ${file.is_public ? 'Yes' : 'No'}\n`;
if (file.description) {
content += `- **Description:** ${file.description}\n`;
}
content += `- **Uploaded:** ${new Date(file.created_at).toLocaleDateString()}\n`;
content += '\n';
});
content += `**Use \`get_file_content\` or \`download_file\` to access these files.**\n`;
}
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error in get_bottleneck: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
},
};
}
/**
* List bottlenecks with basic information
*/
export function createListBottlenecksTool(client) {
return {
type: 'call_tool',
name: 'list_bottlenecks',
description: 'Get a paginated list of bottlenecks with basic information',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number (1-based)',
minimum: 1,
default: 1,
},
limit: {
type: 'number',
description: 'Number of results per page',
minimum: 1,
maximum: 100,
default: 20,
},
sort_by: {
type: 'string',
description: 'Sort field',
enum: ['created_at', 'updated_at', 'title'],
default: 'created_at',
},
sort_order: {
type: 'string',
description: 'Sort order',
enum: ['asc', 'desc'],
default: 'desc',
},
public_only: {
type: 'boolean',
description: 'Only show public bottlenecks',
default: false,
},
include_unapproved: {
type: 'boolean',
description: 'Include unapproved cards (admin only)',
default: false,
},
approval_status: {
type: 'string',
description: 'Filter by approval status (admin only)',
enum: ['approved', 'pending', 'rejected', 'draft'],
},
},
required: [],
},
handler: async (args) => {
try {
const params = new URLSearchParams();
if (args.page)
params.append('page', args.page.toString());
if (args.limit)
params.append('limit', args.limit.toString());
if (args.sort_by)
params.append('sort_by', args.sort_by);
if (args.sort_order)
params.append('sort_order', args.sort_order);
if (args.public_only)
params.append('is_public', 'true');
if (args.approval_status)
params.append('status', args.approval_status);
// Use admin endpoint if accessing unapproved content or specific approval status
const useAdminEndpoint = args.include_unapproved || args.approval_status;
const endpoint = useAdminEndpoint
? `/api/admin/cards?${params.toString()}`
: `/api/cards?${params.toString()}`;
const response = await client.request(endpoint, {
method: 'GET',
requireAuth: !args.public_only || useAdminEndpoint,
});
if (!response.success) {
return {
content: [
{
type: 'text',
text: `Error listing bottlenecks: ${response.error}`,
},
],
isError: true,
};
}
const data = response.data || {};
let content = `# Bottlenecks List\n\n`;
// Handle both regular API response (data.cards) and admin API response (data array or data.data)
const cards = data.cards || data.data || data || [];
if (!Array.isArray(cards) || cards.length === 0) {
content += `No bottlenecks found.\n`;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
const total = data.total || data.pagination?.total || cards.length;
const page = data.pagination?.page || args.page || 1;
const limit = data.pagination?.limit || args.limit || 20;
content += `**Total:** ${total} bottlenecks\n`;
content += `**Page:** ${page} of ${Math.ceil(total / limit)}\n`;
content += `**Showing:** ${cards.length} results\n\n`;
// Create a table-like format
content += `| # | Title | Status | Created | Tags | Files |\n`;
content += `|---|-------|--------|---------|------|-------|\n`;
cards.forEach((bottleneck, index) => {
const rowNum = (page - 1) * limit + index + 1;
const title = bottleneck.title.length > 40
? bottleneck.title.substring(0, 37) + '...'
: bottleneck.title;
const status = bottleneck.status || 'unknown';
const created = new Date(bottleneck.created_at).toLocaleDateString();
const tags = bottleneck.tags && bottleneck.tags.length > 0
? bottleneck.tags.slice(0, 2).join(', ') +
(bottleneck.tags.length > 2 ? '...' : '')
: '-';
const files = bottleneck.file_attachments
? bottleneck.file_attachments.length
: 0;
content += `| ${rowNum} | ${title} | ${status} | ${created} | ${tags} | ${files} |\n`;
});
content += '\n';
// Navigation info
if (data.page > 1) {
content += `**Previous page:** Use \`page: ${data.page - 1}\`\n`;
}
if (data.hasMore) {
content += `**Next page:** Use \`page: ${data.page + 1}\`\n`;
}
content += `\n**Use \`get_bottleneck\` with an ID to see full details.**\n`;
content += `**Use \`search_bottlenecks\` for filtered results.**\n`;
return {
content: [
{
type: 'text',
text: content,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error in list_bottlenecks: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
},
};
}
//# sourceMappingURL=read-operations.js.map