Xano MCP Server

  • src
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; // Configuration - HARDCODED VALUES - Replace with actual values const XANO_API_KEY = "metadata_api_key_here"; // REPLACE WITH YOUR ACTUAL API KEY const XANO_WORKSPACE = 1; // REPLACE WITH YOUR ACTUAL WORKSPACE ID const XANO_API_BASE = "https://your-url.xano.io/api:meta"; // REPLACE IF YOUR BASE URL IS DIFFERENT // Types interface DatabaseRecord { id: string; [key: string]: any; } // Xano specific types interface XanoTable { id: string; name: string; description?: string; tags?: string[]; created_at: string; updated_at: string; } interface XanoTableSchema { name: string; type: string; description?: string; required?: boolean; nullable?: boolean; access?: 'public' | 'private' | 'internal'; config?: Record<string, any>; } // API Group and API interfaces interface XanoApiGroup { id: string; name: string; description?: string; docs?: string; created_at: string; updated_at: string; guid?: string; canonical?: string; swagger?: boolean; documentation?: { require_token: boolean; token: string; link: string; }; branch?: string; tag?: string[]; } interface XanoApi { id: string; name: string; description: string; docs?: string; guid?: string; created_at: string; updated_at: string; verb: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH' | 'HEAD'; tag?: string[]; cache?: { active: boolean; ttl: number; input: boolean; auth: boolean; datasource: boolean; ip: boolean; headers: string[]; env?: string[]; }; auth?: Record<string, any>; input?: any[]; } // Server configuration const SERVER_CONFIG = { name: "xano-mcp", version: "1.0.0", description: "MCP server for interacting with Xano database and APIs", }; // Create server instance with better configuration console.error(`[Setup] Creating MCP server: ${SERVER_CONFIG.name} v${SERVER_CONFIG.version}`); const server = new McpServer({ name: SERVER_CONFIG.name, version: SERVER_CONFIG.version, }); // Helper function for making Xano API requests async function makeXanoRequest<T>(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', body?: any): Promise<T> { try { console.error(`[API] Making ${method} request to endpoint: ${endpoint}`); if (body) { console.error(`[API] Request body: ${JSON.stringify(body, null, 2)}`); } const url = new URL(`${XANO_API_BASE}${endpoint}`); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${XANO_API_KEY}`, 'X-Workspace': String(XANO_WORKSPACE) }; const response = await fetch(url.toString(), { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const errorText = await response.text(); console.error(`[Error] HTTP error! status: ${response.status}, response: ${errorText}`); throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`); } const data = await response.json(); console.error(`[API] Successfully received response from endpoint: ${endpoint}`); return data; } catch (error) { console.error(`[Error] Failed to make Xano request to ${endpoint}: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // List Tables Tool server.tool( "list-tables", "Browse all tables in the Xano workspace", {}, async () => { console.error('[Tool] Executing list-tables'); try { const response = await makeXanoRequest<{ items: XanoTable[] }>(`/workspace/${XANO_WORKSPACE}/table`); const tables = response.items; // Access the 'items' property // Format tables into a more readable structure const formattedContent = `# Xano Database Tables\n\n${tables.map(table => `## ${table.name}\n` + `**ID**: ${table.id}\n` + `**Description**: ${table.description || 'No description'}\n` + `**Created**: ${new Date(table.created_at).toLocaleString()}\n` + `**Updated**: ${new Date(table.updated_at).toLocaleString()}\n` + `${table.tags && table.tags.length > 0 ? `**Tags**: ${table.tags.join(', ')}\n` : ''}` ).join('\n\n')}`; console.error(`[Tool] Successfully listed ${tables.length} tables`); return { content: [ { type: "text", text: formattedContent } ] }; } catch (error) { console.error(`[Error] Failed to list tables: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error listing tables: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Get Table Schema Tool server.tool( "get-table-schema", "Browse the schema of a table", { table_id: z.string().describe("ID of the table to get schema from"), format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: 'markdown' for readable documentation or 'json' for complete schema") }, async ({ table_id, format }) => { console.error(`[Tool] Executing get-table-schema for table ID: ${table_id} with format: ${format}`); try { const schema = await makeXanoRequest(`/workspace/${XANO_WORKSPACE}/table/${table_id}/schema`); if (format === "json") { // Return the complete JSON schema return { content: [ { type: "text", text: `# Table Schema (Full JSON)\n\n\`\`\`json\n${JSON.stringify(schema, null, 2)}\n\`\`\`` } ] }; } else { // Format schema into readable structure const formattedContent = `# Schema for Table ID: ${table_id}\n\n` + (Array.isArray(schema) ? schema.map(field => { let content = `## ${field.name} (${field.type})\n`; content += `**Required**: ${field.required ? 'Yes' : 'No'}\n`; content += `**Nullable**: ${field.nullable ? 'Yes' : 'No'}\n`; content += `**Access**: ${field.access || 'public'}\n`; content += `**Style**: ${field.style || 'single'}\n`; if (field.description) content += `**Description**: ${field.description}\n`; if (field.default !== undefined) content += `**Default**: ${field.default}\n`; if (field.config && Object.keys(field.config).length > 0) { content += `**Config**:\n\`\`\`json\n${JSON.stringify(field.config, null, 2)}\n\`\`\`\n`; } if (field.validators && Object.keys(field.validators).length > 0) { content += `**Validators**:\n\`\`\`json\n${JSON.stringify(field.validators, null, 2)}\n\`\`\`\n`; } if (field.children && field.children.length > 0) { content += `**Children**:\n\`\`\`json\n${JSON.stringify(field.children, null, 2)}\n\`\`\`\n`; } return content; }).join('\n\n') : `Error: Unexpected schema format: ${JSON.stringify(schema)}` ); console.error(`[Tool] Successfully retrieved schema for table ID: ${table_id}`); return { content: [ { type: "text", text: formattedContent } ] }; } } catch (error) { console.error(`[Error] Failed to get table schema: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error getting table schema: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Add Table Tool with Complete Schema Support server.tool( "add-table", "Add a new table to the Xano database", { name: z.string().describe("Name of the table"), description: z.string().optional().describe("Description of the table"), schema: z.array(z.object({ name: z.string().describe("Name of the schema element"), type: z.enum([ "attachment", "audio", "bool", "date", "decimal", "email", "enum", "geo_linestring", "geo_multilinestring", "geo_multipoint", "geo_multipolygon", "geo_point", "geo_polygon", "image", "int", "json", "object", "password", "tablerefuuid", "text", "timestamp", "uuid", "vector", "video" ]).describe("Type of the schema element"), description: z.string().optional().describe("Description of the schema element"), nullable: z.boolean().optional().default(false).describe("Whether the field can be null"), required: z.boolean().optional().default(false).describe("Whether the field is required"), access: z.enum(["public", "private", "internal"]).optional().default("public").describe("Access level for the field"), style: z.enum(["single", "list"]).optional().default("single").describe("Whether the field is a single value or a list"), default: z.string().optional().describe("Default value for the field"), config: z.record(z.any()).optional().describe("Additional configuration for specific field types"), validators: z.object({ lower: z.boolean().optional(), max: z.number().optional(), maxLength: z.number().optional(), min: z.number().optional(), minLength: z.number().optional(), pattern: z.string().optional(), precision: z.number().optional(), scale: z.number().optional(), trim: z.boolean().optional() }).optional().describe("Validation rules for the field"), children: z.array(z.any()).optional().describe("Nested fields for object types"), tableref_id: z.string().optional().describe("ID of the referenced table (only valid when type is 'int')"), values: z.array(z.string()).optional().describe("Array of allowed values (only for enum type)") })).optional().describe(`Schema configuration for the table. For foreign key relationships, use type 'int' with tableref_id. Example: { "name": "contact_id", "type": "int", "description": "Reference to contact table", "nullable": false, "required": false, "access": "public", "style": "single", "default": "0", "tableref_id": "100" // ID of the table to reference }`) }, async ({ name, description, schema }) => { console.error(`[Tool] Executing add-table for table: ${name}`); try { // Step 1: Create the table const createTableResponse = await makeXanoRequest<{ id: string }>( `/workspace/${XANO_WORKSPACE}/table`, 'POST', { name, description } ); const tableId = createTableResponse.id; console.error(`[Tool] Table created with ID: ${tableId}`); // Step 2: If schema is provided, process and add it to the table if (schema && schema.length > 0) { try { // Process schema fields to handle relationships const processedSchema = schema.map(field => { // Validate relationship fields if (field.tableref_id && field.type !== "int") { throw new Error(`Field "${field.name}" has tableref_id but type is not "int". Foreign key fields must be of type "int".`); } return field; }); // Update the schema with processed fields await makeXanoRequest( `/workspace/${XANO_WORKSPACE}/table/${tableId}/schema`, 'PUT', { schema: processedSchema } ); console.error(`[Tool] Schema successfully added to table ID: ${tableId}`); } catch (schemaError) { console.error(`[Error] Failed to add schema: ${schemaError instanceof Error ? schemaError.message : String(schemaError)}`); return { content: [ { type: "text", text: `Table created with ID ${tableId}, but failed to add schema: ${schemaError instanceof Error ? schemaError.message : String(schemaError)}` } ], isError: true }; } } return { content: [ { type: "text", text: `Successfully created table "${name}" with ID: ${tableId}${schema ? ' and added the specified schema.' : '.'}` } ] }; } catch (error) { console.error(`[Error] Failed to create table: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error creating table: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Delete Table Tool /* server.tool( "delete-table", "Delete a table from the Xano workspace", { table_id: z.string().describe("ID of the table to delete") }, async ({ table_id }) => { console.error(`[Tool] Executing delete-table for table ID: ${table_id}`); try { await makeXanoRequest(`/workspace/${XANO_WORKSPACE}/table/${table_id}`, 'DELETE'); console.error(`[Tool] Successfully deleted table ID: ${table_id}`); return { content: [ { type: "text", text: `Successfully deleted table with ID: ${table_id}` } ] }; } catch (error) { console.error(`[Error] Failed to delete table: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error deleting table: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); */ // Edit Table Schema Tool server.tool( "edit-table-schema", "Edit the schema of an existing table (add, remove, or modify columns)", { table_id: z.string().describe("ID of the table to edit"), operation: z.enum(['update', 'add_column', 'rename_column', 'remove_column']).describe("Type of schema operation to perform"), // For updating the entire schema schema: z.array(z.object({ name: z.string().describe("Name of the schema element"), type: z.enum([ "attachment", "audio", "bool", "date", "decimal", "email", "enum", "geo_linestring", "geo_multilinestring", "geo_multipoint", "geo_multipolygon", "geo_point", "geo_polygon", "image", "int", "json", "object", "password", "tableref", "tablerefuuid", "text", "timestamp", "uuid", "vector", "video" ]).describe("Type of the schema element"), description: z.string().optional().describe("Description of the schema element"), nullable: z.boolean().optional().default(false).describe("Whether the field can be null"), required: z.boolean().optional().default(false).describe("Whether the field is required"), access: z.enum(["public", "private", "internal"]).optional().default("public").describe("Access level for the field"), style: z.enum(["single", "list"]).optional().default("single").describe("Whether the field is a single value or a list"), default: z.string().optional().describe("Default value for the field"), config: z.record(z.any()).optional().describe("Additional configuration for specific field types"), children: z.array(z.any()).optional().describe("Nested fields for object types") })).optional().describe("Full schema specification (for 'update' operation)"), // For adding a single column column: z.object({ name: z.string().describe("Name of the column"), type: z.enum([ "attachment", "audio", "bool", "date", "decimal", "email", "enum", "geo_linestring", "geo_multilinestring", "geo_multipoint", "geo_multipolygon", "geo_point", "geo_polygon", "image", "int", "json", "object", "password", "tableref", "tablerefuuid", "text", "timestamp", "uuid", "vector", "video" ]).describe("Type of the column"), description: z.string().optional().describe("Description of the column"), nullable: z.boolean().optional().default(false).describe("Whether the field can be null"), required: z.boolean().optional().default(false).describe("Whether the field is required"), access: z.enum(["public", "private", "internal"]).optional().default("public").describe("Access level for the field"), style: z.enum(["single", "list"]).optional().default("single").describe("Whether the field is a single value or a list"), default: z.string().optional().describe("Default value for the field"), config: z.record(z.any()).optional().describe("Additional configuration for the column") }).optional().describe("Column specification (for 'add_column' operation)"), // For renaming a column rename: z.object({ old_name: z.string().describe("Current name of the column"), new_name: z.string().describe("New name for the column") }).optional().describe("Rename specification (for 'rename_column' operation)"), // For removing a column column_name: z.string().optional().describe("Name of the column to remove (for 'remove_column' operation)") }, async ({ table_id, operation, schema, column, rename, column_name }) => { console.error(`[Tool] Executing edit-table-schema for table ID: ${table_id}, operation: ${operation}`); try { let result; let successMessage = ""; switch (operation) { case 'update': if (!schema || schema.length === 0) { return { content: [{ type: "text", text: "Error: Schema array must be provided for 'update' operation" }], isError: true }; } // PUT request to update the entire schema await makeXanoRequest( `/workspace/${XANO_WORKSPACE}/table/${table_id}/schema`, 'PUT', { schema } ); successMessage = `Successfully updated the entire schema for table ID: ${table_id}`; break; case 'add_column': if (!column) { return { content: [{ type: "text", text: "Error: Column specification must be provided for 'add_column' operation" }], isError: true }; } // POST request to add a new column of the specified type await makeXanoRequest( `/workspace/${XANO_WORKSPACE}/table/${table_id}/schema/type/${column.type}`, 'POST', column ); successMessage = `Successfully added column '${column.name}' of type '${column.type}' to table ID: ${table_id}`; break; case 'rename_column': if (!rename) { return { content: [{ type: "text", text: "Error: Rename specification must be provided for 'rename_column' operation" }], isError: true }; } // POST request to rename a column await makeXanoRequest( `/workspace/${XANO_WORKSPACE}/table/${table_id}/schema/rename`, 'POST', rename ); successMessage = `Successfully renamed column from '${rename.old_name}' to '${rename.new_name}' in table ID: ${table_id}`; break; case 'remove_column': if (!column_name) { return { content: [{ type: "text", text: "Error: Column name must be provided for 'remove_column' operation" }], isError: true }; } // DELETE request to remove a column await makeXanoRequest( `/workspace/${XANO_WORKSPACE}/table/${table_id}/schema/${column_name}`, 'DELETE' ); successMessage = `Successfully removed column '${column_name}' from table ID: ${table_id}`; break; } console.error(`[Tool] ${successMessage}`); return { content: [{ type: "text", text: successMessage }] }; } catch (error) { console.error(`[Error] Failed to edit table schema: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error editing table schema: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Get API Specification Tool server.tool( "get-api-spec", "Get and convert Swagger specification for an API group to a minified markdown format", { apigroup_id: z.string().describe("ID of the API group to get specification for"), format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: 'markdown' for concise documentation or 'json' for full specification") }, async ({ apigroup_id, format }) => { console.error(`[Tool] Executing get-api-spec for API group ID: ${apigroup_id} with format: ${format}`); try { // Step 1: Get the API group details to find the Swagger spec link const apiGroup = await makeXanoRequest<XanoApiGroup>(`/workspace/${XANO_WORKSPACE}/apigroup/${apigroup_id}`); if (!apiGroup.swagger || !apiGroup.documentation || !apiGroup.documentation.link) { return { content: [ { type: "text", text: `API group (ID: ${apigroup_id}) does not have Swagger documentation available.` } ], isError: true }; } console.error(`[Tool] Found Swagger spec link: ${apiGroup.documentation.link}`); // Step 2: Fetch the Swagger JSON specification const swaggerResponse = await fetch(apiGroup.documentation.link); if (!swaggerResponse.ok) { throw new Error(`Failed to fetch Swagger spec: ${swaggerResponse.statusText}`); } const swaggerSpec = await swaggerResponse.json(); console.error(`[Tool] Successfully retrieved Swagger specification`); // Step 3: Process the spec based on format if (format === "json") { // Return the full JSON specification return { content: [ { type: "text", text: `# ${apiGroup.name} API Specification (Full JSON)\n\n\`\`\`json\n${JSON.stringify(swaggerSpec, null, 2)}\n\`\`\`` } ] }; } else { // Process the Swagger spec into a minified markdown format const markdown = processSwaggerToMarkdown(swaggerSpec, apiGroup.name); return { content: [ { type: "text", text: markdown } ] }; } } catch (error) { console.error(`[Error] Failed to get API spec: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error getting API specification: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Helper function to process Swagger spec into concise markdown function processSwaggerToMarkdown(swaggerSpec: any, apiGroupName: string): string { try { console.error(`[Process] Converting Swagger spec to markdown for: ${apiGroupName}`); // Extract basic API information const info = swaggerSpec.info || {}; const server = swaggerSpec.servers?.[0]?.url || 'https://'; const baseUrl = server; // Build the markdown content let markdown = `# ${apiGroupName} API\n\n`; markdown += `## API Info\n`; markdown += `- Title: ${info.title || apiGroupName}\n`; markdown += `- Version: ${info.version || 'N/A'}\n`; markdown += `- Base URL: ${baseUrl}\n\n`; // Common responses markdown += `## Responses\n`; markdown += `| Code | Description |\n`; markdown += `|------|-------------|\n`; markdown += `| 200 | Success! |\n`; markdown += `| 400 | Input Error |\n`; markdown += `| 401 | Unauthorized|\n`; markdown += `| 403 | Access Denied|\n`; markdown += `| 404 | Not Found |\n`; markdown += `| 429 | Rate Limited|\n`; markdown += `| 500 | Server Error|\n\n`; // Process endpoints markdown += `## Endpoints\n\n`; const paths = swaggerSpec.paths || {}; const pathKeys = Object.keys(paths).sort(); for (const path of pathKeys) { const pathInfo = paths[path]; const methods = Object.keys(pathInfo).filter(m => ['get', 'post', 'put', 'delete', 'patch'].includes(m.toLowerCase())); for (const method of methods) { const operation = pathInfo[method]; markdown += `### ${method.toUpperCase()} ${path}\n`; markdown += `${operation.summary || 'No summary'}\n`; // Parameters const parameters = operation.parameters || []; if (parameters.length > 0) { markdown += `| Param | In | Req | Type |\n`; markdown += `|-------|----|-----|------|\n`; for (const param of parameters) { markdown += `| ${param.name} | ${param.in} | ${param.required ? 'Y' : 'N'} | ${param.schema?.type || 'unknown'} |\n`; } } markdown += '\n'; } } // Authentication const securitySchemes = swaggerSpec.components?.securitySchemes || {}; if (Object.keys(securitySchemes).length > 0) { markdown += `## Auth\n`; for (const name in securitySchemes) { const scheme = securitySchemes[name]; markdown += `- ${name}: ${scheme.type}`; if (scheme.scheme) markdown += ` (${scheme.scheme})`; markdown += '\n'; } } console.error(`[Process] Successfully converted Swagger spec to markdown`); return markdown; } catch (error) { console.error(`[Error] Error processing Swagger to Markdown: ${error instanceof Error ? error.message : String(error)}`); return `# Error\n\n${error instanceof Error ? error.message : String(error)}`; } } // Create API Group Tool server.tool( "create-api-group", "Create a new API group in the Xano workspace", { name: z.string().describe("Name of the API group"), description: z.string().describe("Description of the API group"), swagger: z.boolean().describe("Whether to enable Swagger documentation"), docs: z.string().optional().describe("Documentation for the API group"), tag: z.array(z.string()).optional().nullable().describe("Tags to associate with the API group"), branch: z.string().optional().describe("Branch name for the API group") }, async ({ name, description, swagger, docs, tag, branch }) => { console.error(`[Tool] Executing create-api-group for name: ${name}`); try { const requestBody = { name, description, swagger, ...(docs !== undefined && { docs }), ...(tag !== undefined && { tag }), ...(branch !== undefined && { branch }) }; const response = await makeXanoRequest<XanoApiGroup>( `/workspace/${XANO_WORKSPACE}/apigroup`, 'POST', requestBody ); console.error(`[Tool] Successfully created API group "${name}" with ID: ${response.id}`); const formattedContent = `# API Group Created\n\n` + `**Name**: ${response.name}\n` + `**ID**: ${response.id}\n` + `**Description**: ${response.description || 'No description'}\n` + `${response.docs ? `**Documentation**: ${response.docs}\n` : ''}` + `**Swagger Documentation**: ${response.swagger ? 'Enabled' : 'Disabled'}\n` + `**Created**: ${new Date(response.created_at).toLocaleString()}\n` + `**Updated**: ${new Date(response.updated_at).toLocaleString()}\n` + `${response.guid ? `**GUID**: ${response.guid}\n` : ''}` + `${response.canonical ? `**Canonical**: ${response.canonical}\n` : ''}` + `${response.branch ? `**Branch**: ${response.branch}\n` : ''}` + `${response.tag && response.tag.length > 0 ? `**Tags**: ${response.tag.join(', ')}\n` : ''}`; return { content: [ { type: "text", text: formattedContent } ] }; } catch (error) { console.error(`[Error] Failed to create API group: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error creating API group: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // List API Groups Tool server.tool( "list-api-groups", "Browse all API groups in the Xano workspace", { page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), search: z.string().optional().describe("Search term to filter API groups"), sort: z.enum(["created_at", "updated_at", "name"]).optional().describe("Field to sort by"), order: z.enum(["asc", "desc"]).optional().describe("Sort order") }, async ({ page, per_page, search, sort, order }) => { console.error('[Tool] Executing list-api-groups'); try { // Build query parameters const queryParams = new URLSearchParams(); if (page !== undefined) queryParams.append("page", page.toString()); if (per_page !== undefined) queryParams.append("per_page", per_page.toString()); if (search) queryParams.append("search", search); if (sort) queryParams.append("sort", sort); if (order) queryParams.append("order", order); const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; const response = await makeXanoRequest<{ items: XanoApiGroup[], curPage: number, nextPage?: number, prevPage?: number }>( `/workspace/${XANO_WORKSPACE}/apigroup${queryString}` ); const apiGroups = response.items; // Format API groups into a readable structure const formattedContent = `# Xano API Groups\n\n` + `Page ${response.curPage}${response.nextPage ? ` (Next: ${response.nextPage})` : ''}${response.prevPage ? ` (Prev: ${response.prevPage})` : ''}\n\n` + `${apiGroups.map(group => `## ${group.name}\n` + `**ID**: ${group.id}\n` + `**Description**: ${group.description || 'No description'}\n` + `**Created**: ${new Date(group.created_at).toLocaleString()}\n` + `**Updated**: ${new Date(group.updated_at).toLocaleString()}\n` + `${group.guid ? `**GUID**: ${group.guid}\n` : ''}` ).join('\n\n')}`; console.error(`[Tool] Successfully listed ${apiGroups.length} API groups`); return { content: [ { type: "text", text: formattedContent } ] }; } catch (error) { console.error(`[Error] Failed to list API groups: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error listing API groups: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Browse APIs in an API Group Tool server.tool( "browse-apis", "Browse APIs in a specific API group", { apigroup_id: z.string().describe("ID of the API group to browse"), page: z.number().optional().describe("Page number for pagination"), per_page: z.number().optional().describe("Number of items per page"), search: z.string().optional().describe("Search term to filter APIs"), sort: z.enum(["created_at", "updated_at", "name"]).optional().describe("Field to sort by"), order: z.enum(["asc", "desc"]).optional().describe("Sort order") }, async ({ apigroup_id, page, per_page, search, sort, order }) => { console.error(`[Tool] Executing browse-apis for API group ID: ${apigroup_id}`); try { // Build query parameters const queryParams = new URLSearchParams(); if (page !== undefined) queryParams.append("page", page.toString()); if (per_page !== undefined) queryParams.append("per_page", per_page.toString()); if (search) queryParams.append("search", search); if (sort) queryParams.append("sort", sort); if (order) queryParams.append("order", order); const queryString = queryParams.toString() ? `?${queryParams.toString()}` : ''; const response = await makeXanoRequest<{ items: XanoApi[], curPage: number, nextPage?: number, prevPage?: number }>( `/workspace/${XANO_WORKSPACE}/apigroup/${apigroup_id}/api${queryString}` ); const apis = response.items; // Format APIs into a readable structure const formattedContent = `# APIs in API Group ID: ${apigroup_id}\n\n` + `Page ${response.curPage}${response.nextPage ? ` (Next: ${response.nextPage})` : ''}${response.prevPage ? ` (Prev: ${response.prevPage})` : ''}\n\n` + `${apis.map(api => `## ${api.name}\n` + `**ID**: ${api.id}\n` + `**Verb**: ${api.verb}\n` + `**Description**: ${api.description || 'No description'}\n` + `${api.docs ? `**Documentation**: ${api.docs}\n` : ''}` + `**Created**: ${new Date(api.created_at).toLocaleString()}\n` + `**Updated**: ${new Date(api.updated_at).toLocaleString()}\n` + `${api.guid ? `**GUID**: ${api.guid}\n` : ''}` + `${api.tag && api.tag.length > 0 ? `**Tags**: ${api.tag.join(', ')}\n` : ''}` ).join('\n\n')}`; console.error(`[Tool] Successfully listed ${apis.length} APIs for API group ID: ${apigroup_id}`); return { content: [ { type: "text", text: formattedContent } ] }; } catch (error) { console.error(`[Error] Failed to browse APIs: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error browsing APIs: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Add API to API Group Tool server.tool( "add-api", "Add a new API to an API group", { apigroup_id: z.string().describe("ID of the API group to add the API to"), name: z.string().describe("Name of the API"), description: z.string().describe("Description of the API"), docs: z.string().optional().describe("Documentation for the API"), verb: z.enum(["GET", "POST", "DELETE", "PUT", "PATCH", "HEAD"]).describe("HTTP verb for the API"), tag: z.array(z.string()).optional().describe("Tags to associate with the API") }, async ({ apigroup_id, name, description, docs, verb, tag }) => { console.error(`[Tool] Executing add-api for API group ID: ${apigroup_id}`); try { const requestBody = { name, description, verb, ...(docs !== undefined && { docs }), ...(tag !== undefined && { tag }) }; const response = await makeXanoRequest<XanoApi>( `/workspace/${XANO_WORKSPACE}/apigroup/${apigroup_id}/api`, 'POST', requestBody ); console.error(`[Tool] Successfully added API "${name}" with ID: ${response.id} to API group ID: ${apigroup_id}`); const formattedContent = `# API Added\n\n` + `**Name**: ${response.name}\n` + `**ID**: ${response.id}\n` + `**API Group ID**: ${apigroup_id}\n` + `**Verb**: ${response.verb}\n` + `**Description**: ${response.description}\n` + `${response.docs ? `**Documentation**: ${response.docs}\n` : ''}` + `**Created**: ${new Date(response.created_at).toLocaleString()}\n` + `${response.guid ? `**GUID**: ${response.guid}\n` : ''}` + `${response.tag && response.tag.length > 0 ? `**Tags**: ${response.tag.join(', ')}\n` : ''}`; return { content: [ { type: "text", text: formattedContent } ] }; } catch (error) { console.error(`[Error] Failed to add API: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: "text", text: `Error adding API: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Start the server async function main() { console.error("Starting MCP server..."); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Xano MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });