diff --git a/Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts b/Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts index a21a2ed16b..49deccdc73 100644 --- a/Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +++ b/Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts @@ -657,8 +657,7 @@ export default class DatabaseBaseModel extends BaseEntity { const baseModel: T = new type(); for (let key of Object.keys(json)) { - - if(key === "id") { + if (key === "id") { key = "_id"; json["_id"] = json["id"]; delete json["id"]; diff --git a/Common/Models/DatabaseModels/User.ts b/Common/Models/DatabaseModels/User.ts index eac19b634d..8cf546f536 100644 --- a/Common/Models/DatabaseModels/User.ts +++ b/Common/Models/DatabaseModels/User.ts @@ -439,7 +439,10 @@ class User extends UserModel { update: [], }) - @TableColumn({ type: TableColumnType.ShortText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.ShortText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.ShortText, length: ColumnLength.ShortText, @@ -560,7 +563,10 @@ class User extends UserModel { read: [], update: [], }) - @TableColumn({ type: TableColumnType.LongText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.LongText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.LongText, length: ColumnLength.LongText, @@ -574,7 +580,10 @@ class User extends UserModel { read: [], update: [], }) - @TableColumn({ type: TableColumnType.LongText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.LongText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.LongText, length: ColumnLength.LongText, @@ -588,7 +597,10 @@ class User extends UserModel { read: [], update: [], }) - @TableColumn({ type: TableColumnType.LongText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.LongText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.LongText, length: ColumnLength.LongText, @@ -602,7 +614,10 @@ class User extends UserModel { read: [], update: [], }) - @TableColumn({ type: TableColumnType.LongText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.LongText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.LongText, length: ColumnLength.LongText, @@ -616,7 +631,10 @@ class User extends UserModel { read: [], update: [], }) - @TableColumn({ type: TableColumnType.LongText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.LongText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.LongText, length: ColumnLength.LongText, @@ -630,7 +648,10 @@ class User extends UserModel { read: [], update: [], }) - @TableColumn({ type: TableColumnType.LongText, hideColumnInDocumentation: true }) + @TableColumn({ + type: TableColumnType.LongText, + hideColumnInDocumentation: true, + }) @Column({ type: ColumnType.LongText, length: ColumnLength.LongText, diff --git a/Common/Server/API/BaseAPI.ts b/Common/Server/API/BaseAPI.ts index 0468911dc8..e2278fcc05 100644 --- a/Common/Server/API/BaseAPI.ts +++ b/Common/Server/API/BaseAPI.ts @@ -403,8 +403,6 @@ export default class BaseAPI< await this.onBeforeCreate(req, res); const body: JSONObject = req.body; - - const item: TBaseModel = BaseModel.fromJSON( body["data"] as JSONObject, this.entityType, diff --git a/Common/Server/Types/Database/QueryUtil.ts b/Common/Server/Types/Database/QueryUtil.ts index 99c7b6758e..448880ad87 100644 --- a/Common/Server/Types/Database/QueryUtil.ts +++ b/Common/Server/Types/Database/QueryUtil.ts @@ -54,7 +54,7 @@ export default class QueryUtil { query[key] = QueryHelper.equalToOrNull( query[key] as any, ) as FindOperator as any; - }else if ( + } else if ( query[key] && query[key] instanceof EqualTo && tableColumnMetadata diff --git a/Common/Utils/Schema/AnalyticsModelSchema.ts b/Common/Utils/Schema/AnalyticsModelSchema.ts index 47ca590231..3d577d9a30 100644 --- a/Common/Utils/Schema/AnalyticsModelSchema.ts +++ b/Common/Utils/Schema/AnalyticsModelSchema.ts @@ -376,7 +376,10 @@ export class AnalyticsModelSchema extends BaseSchema { continue; } - let zodType: ZodTypes.ZodTypeAny = this.getZodTypeForColumn(column, data.disableOpenApiSchema || false); + let zodType: ZodTypes.ZodTypeAny = this.getZodTypeForColumn( + column, + data.disableOpenApiSchema || false, + ); // Apply default value if it exists zodType = this.applyDefaultValue(zodType, column); diff --git a/Common/Utils/Schema/ModelSchema.ts b/Common/Utils/Schema/ModelSchema.ts index be4d89468c..dc362a98aa 100644 --- a/Common/Utils/Schema/ModelSchema.ts +++ b/Common/Utils/Schema/ModelSchema.ts @@ -1236,7 +1236,10 @@ export class ModelSchema extends BaseSchema { }; // Helper function to conditionally apply OpenAPI schema - const applyOpenApi = (baseType: ZodTypes.ZodTypeAny, openApiConfig: any): ZodTypes.ZodTypeAny => { + const applyOpenApi = ( + baseType: ZodTypes.ZodTypeAny, + openApiConfig: any, + ): ZodTypes.ZodTypeAny => { if (disableOpenApiSchema) { return baseType; } @@ -1464,18 +1467,20 @@ export class ModelSchema extends BaseSchema { }), ); - zodType = disableOpenApiSchema - ? arrayType - : arrayType.openapi(addDefaultToOpenApi({ - type: "array", - items: { - type: "object", - properties: { - id: { type: "string" }, + zodType = disableOpenApiSchema + ? arrayType + : arrayType.openapi( + addDefaultToOpenApi({ + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + }, }, - }, - example: [{ id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }], - })); + example: [{ id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }], + }), + ); } else if (column.type === TableColumnType.Entity) { const entityType: (new () => DatabaseBaseModel) | undefined = column.modelType; @@ -1511,10 +1516,12 @@ export class ModelSchema extends BaseSchema { zodType = disableOpenApiSchema ? lazyType - : lazyType.openapi(addDefaultToOpenApi({ - type: "object", - example: { id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, - })); + : lazyType.openapi( + addDefaultToOpenApi({ + type: "object", + example: { id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, + }), + ); } else { zodType = applyOpenApi(z.any(), { type: "null", example: null }); } diff --git a/MCP/Index.ts b/MCP/Index.ts index 1521c1c57c..6838b8f388 100755 --- a/MCP/Index.ts +++ b/MCP/Index.ts @@ -10,7 +10,9 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import dotenv from "dotenv"; import DynamicToolGenerator from "./Utils/DynamicToolGenerator"; -import OneUptimeApiService, { OneUptimeApiConfig } from "./Services/OneUptimeApiService"; +import OneUptimeApiService, { + OneUptimeApiConfig, +} from "./Services/OneUptimeApiService"; import { McpToolInfo, OneUptimeToolCallArgs } from "./Types/McpTypes"; import OneUptimeOperation from "./Types/OneUptimeOperation"; import MCPLogger from "./Utils/MCPLogger"; @@ -37,7 +39,7 @@ class OneUptimeMCPServer { capabilities: { tools: {}, }, - } + }, ); this.initializeServices(); @@ -47,13 +49,15 @@ class OneUptimeMCPServer { private initializeServices(): void { // Initialize OneUptime API Service - const apiKey = process.env['ONEUPTIME_API_KEY']; + const apiKey = process.env["ONEUPTIME_API_KEY"]; if (!apiKey) { - throw new Error("OneUptime API key is required. Please set ONEUPTIME_API_KEY environment variable."); + throw new Error( + "OneUptime API key is required. Please set ONEUPTIME_API_KEY environment variable.", + ); } const config: OneUptimeApiConfig = { - url: process.env['ONEUPTIME_URL'] || "https://oneuptime.com", + url: process.env["ONEUPTIME_URL"] || "https://oneuptime.com", apiKey: apiKey, }; @@ -74,11 +78,13 @@ class OneUptimeMCPServer { private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { - const mcpTools = this.tools.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - })); + const mcpTools = this.tools.map((tool) => { + return { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + }; + }); MCPLogger.info(`Listing ${mcpTools.length} available tools`); return { tools: mcpTools }; @@ -90,12 +96,11 @@ class OneUptimeMCPServer { try { // Find the tool by name - const tool = this.tools.find((t) => t.name === name); + const tool = this.tools.find((t) => { + return t.name === name; + }); if (!tool) { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown tool: ${name}` - ); + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } MCPLogger.info(`Executing tool: ${name} for model: ${tool.modelName}`); @@ -106,11 +111,15 @@ class OneUptimeMCPServer { tool.operation, tool.modelType, tool.apiPath || "", - args as OneUptimeToolCallArgs + args as OneUptimeToolCallArgs, ); // Format the response - const responseText = this.formatToolResponse(tool, result, args as OneUptimeToolCallArgs); + const responseText = this.formatToolResponse( + tool, + result, + args as OneUptimeToolCallArgs, + ); return { content: [ @@ -122,20 +131,24 @@ class OneUptimeMCPServer { }; } catch (error) { MCPLogger.error(`Error executing tool ${name}: ${error}`); - + if (error instanceof McpError) { throw error; } - + throw new McpError( ErrorCode.InternalError, - `Failed to execute ${name}: ${error}` + `Failed to execute ${name}: ${error}`, ); } }); } - private formatToolResponse(tool: McpToolInfo, result: any, args: OneUptimeToolCallArgs): string { + private formatToolResponse( + tool: McpToolInfo, + result: any, + args: OneUptimeToolCallArgs, + ): string { const operation = tool.operation; const modelName = tool.singularName; const pluralName = tool.pluralName; @@ -143,41 +156,42 @@ class OneUptimeMCPServer { switch (operation) { case OneUptimeOperation.Create: return `✅ Successfully created ${modelName}: ${JSON.stringify(result, null, 2)}`; - + case OneUptimeOperation.Read: if (result) { return `📋 Retrieved ${modelName} (ID: ${args.id}): ${JSON.stringify(result, null, 2)}`; - } else { - return `❌ ${modelName} not found with ID: ${args.id}`; } - + return `❌ ${modelName} not found with ID: ${args.id}`; + case OneUptimeOperation.List: const items = Array.isArray(result) ? result : result?.data || []; const count = items.length; const summary = `📊 Found ${count} ${count === 1 ? modelName : pluralName}`; - + if (count === 0) { return `${summary}. No items match the criteria.`; } - + const limitedItems = items.slice(0, 5); // Show first 5 items - const itemsText = limitedItems.map((item: any, index: number) => - `${index + 1}. ${JSON.stringify(item, null, 2)}` - ).join('\n'); - - const hasMore = count > 5 ? `\n... and ${count - 5} more items` : ''; + const itemsText = limitedItems + .map((item: any, index: number) => { + return `${index + 1}. ${JSON.stringify(item, null, 2)}`; + }) + .join("\n"); + + const hasMore = count > 5 ? `\n... and ${count - 5} more items` : ""; return `${summary}:\n${itemsText}${hasMore}`; - + case OneUptimeOperation.Update: return `✅ Successfully updated ${modelName} (ID: ${args.id}): ${JSON.stringify(result, null, 2)}`; - + case OneUptimeOperation.Delete: return `🗑️ Successfully deleted ${modelName} (ID: ${args.id})`; - + case OneUptimeOperation.Count: const totalCount = result?.count || result || 0; return `📊 Total count of ${pluralName}: ${totalCount}`; - + default: return `✅ Operation ${operation} completed successfully: ${JSON.stringify(result, null, 2)}`; } @@ -189,10 +203,12 @@ class OneUptimeMCPServer { MCPLogger.info("OneUptime MCP Server is running!"); MCPLogger.info(`Available tools: ${this.tools.length} total`); - + // Log some example tools - const exampleTools = this.tools.slice(0, 5).map(t => t.name); - MCPLogger.info(`Example tools: ${exampleTools.join(', ')}`); + const exampleTools = this.tools.slice(0, 5).map((t) => { + return t.name; + }); + MCPLogger.info(`Example tools: ${exampleTools.join(", ")}`); } } diff --git a/MCP/Services/OneUptimeApiService.ts b/MCP/Services/OneUptimeApiService.ts index 287c37f533..81572a9602 100644 --- a/MCP/Services/OneUptimeApiService.ts +++ b/MCP/Services/OneUptimeApiService.ts @@ -27,19 +27,21 @@ export default class OneUptimeApiService { public static initialize(config: OneUptimeApiConfig): void { if (!config.apiKey) { - throw new Error("OneUptime API key is required. Please set ONEUPTIME_API_KEY environment variable."); + throw new Error( + "OneUptime API key is required. Please set ONEUPTIME_API_KEY environment variable.", + ); } this.config = config; - + // Parse the URL to extract protocol, hostname, and path const url = URL.fromString(config.url); const protocol = url.protocol; const hostname = url.hostname; - + // Initialize with no base route to avoid route accumulation this.api = new API(protocol, hostname, new Route("/")); - + MCPLogger.info(`OneUptime API Service initialized with: ${config.url}`); } @@ -51,10 +53,12 @@ export default class OneUptimeApiService { operation: OneUptimeOperation, modelType: ModelType, apiPath: string, - args: OneUptimeToolCallArgs + args: OneUptimeToolCallArgs, ): Promise { if (!this.api) { - throw new Error("OneUptime API Service not initialized. Please call initialize() first."); + throw new Error( + "OneUptime API Service not initialized. Please call initialize() first.", + ); } this.validateOperationArgs(operation, args); @@ -63,7 +67,9 @@ export default class OneUptimeApiService { const headers = this.getHeaders(); const data = this.getRequestData(operation, args, tableName, modelType); - MCPLogger.info(`Executing ${operation} operation for ${tableName} at ${route.toString()}`); + MCPLogger.info( + `Executing ${operation} operation for ${tableName} at ${route.toString()}`, + ); try { let response: HTTPResponse | HTTPErrorResponse; @@ -89,21 +95,31 @@ export default class OneUptimeApiService { } if (response instanceof HTTPErrorResponse) { - throw new Error(`API request failed: ${response.statusCode} - ${response.message}`); + throw new Error( + `API request failed: ${response.statusCode} - ${response.message}`, + ); } - MCPLogger.info(`Successfully executed ${operation} operation for ${tableName}`); + MCPLogger.info( + `Successfully executed ${operation} operation for ${tableName}`, + ); return response.data; } catch (error) { - MCPLogger.error(`Error executing ${operation} operation for ${tableName}: ${error}`); + MCPLogger.error( + `Error executing ${operation} operation for ${tableName}: ${error}`, + ); throw error; } } - private static buildApiRoute(apiPath: string, operation: OneUptimeOperation, id?: string): Route { + private static buildApiRoute( + apiPath: string, + operation: OneUptimeOperation, + id?: string, + ): Route { // Start with the API base path let fullPath = `/api${apiPath}`; - + switch (operation) { case OneUptimeOperation.Read: if (id) { @@ -133,15 +149,24 @@ export default class OneUptimeApiService { return new Route(fullPath); } - private static getRequestData(operation: OneUptimeOperation, args: OneUptimeToolCallArgs, tableName: string, modelType: ModelType): JSONObject | undefined { - MCPLogger.info(`Preparing request data for operation: ${operation}, tableName: ${tableName}`); - + private static getRequestData( + operation: OneUptimeOperation, + args: OneUptimeToolCallArgs, + tableName: string, + modelType: ModelType, + ): JSONObject | undefined { + MCPLogger.info( + `Preparing request data for operation: ${operation}, tableName: ${tableName}`, + ); + switch (operation) { case OneUptimeOperation.Create: // For create operations, all properties except reserved ones are part of the data const createData: JSONObject = {}; for (const [key, value] of Object.entries(args)) { - if (!['id', 'query', 'select', 'skip', 'limit', 'sort'].includes(key)) { + if ( + !["id", "query", "select", "skip", "limit", "sort"].includes(key) + ) { createData[key] = value; } } @@ -150,14 +175,17 @@ export default class OneUptimeApiService { // For update operations, all properties except reserved ones are part of the data const updateData: JSONObject = {}; for (const [key, value] of Object.entries(args)) { - if (!['id', 'query', 'select', 'skip', 'limit', 'sort'].includes(key)) { + if ( + !["id", "query", "select", "skip", "limit", "sort"].includes(key) + ) { updateData[key] = value; } } return { data: updateData } as JSONObject; case OneUptimeOperation.List: case OneUptimeOperation.Count: - const generatedSelect = args.select || this.generateAllFieldsSelect(tableName, modelType); + const generatedSelect = + args.select || this.generateAllFieldsSelect(tableName, modelType); const requestData = { query: args.query || {}, select: generatedSelect, @@ -165,16 +193,21 @@ export default class OneUptimeApiService { limit: args.limit, sort: args.sort, } as JSONObject; - - MCPLogger.info(`Request data for ${operation}: ${JSON.stringify(requestData, null, 2)}`); + + MCPLogger.info( + `Request data for ${operation}: ${JSON.stringify(requestData, null, 2)}`, + ); return requestData; case OneUptimeOperation.Read: - const readSelect = args.select || this.generateAllFieldsSelect(tableName, modelType); + const readSelect = + args.select || this.generateAllFieldsSelect(tableName, modelType); const readRequestData = { select: readSelect, } as JSONObject; - - MCPLogger.info(`Request data for Read: ${JSON.stringify(readRequestData, null, 2)}`); + + MCPLogger.info( + `Request data for Read: ${JSON.stringify(readRequestData, null, 2)}`, + ); return readRequestData; case OneUptimeOperation.Delete: default: @@ -185,12 +218,17 @@ export default class OneUptimeApiService { /** * Generate a select object that includes all fields from the select schema */ - private static generateAllFieldsSelect(tableName: string, modelType: ModelType): JSONObject { - MCPLogger.info(`Generating select for tableName: ${tableName}, modelType: ${modelType}`); - + private static generateAllFieldsSelect( + tableName: string, + modelType: ModelType, + ): JSONObject { + MCPLogger.info( + `Generating select for tableName: ${tableName}, modelType: ${modelType}`, + ); + try { let ModelClass: any = null; - + // Find the model class by table name if (modelType === ModelType.Database) { MCPLogger.info(`Searching DatabaseModels for tableName: ${tableName}`); @@ -198,7 +236,9 @@ export default class OneUptimeApiService { try { const instance = new Model(); const instanceTableName = instance.tableName; - MCPLogger.info(`Checking model ${Model.name} with tableName: ${instanceTableName}`); + MCPLogger.info( + `Checking model ${Model.name} with tableName: ${instanceTableName}`, + ); return instanceTableName === tableName; } catch (error) { MCPLogger.warn(`Error instantiating model ${Model.name}: ${error}`); @@ -212,123 +252,157 @@ export default class OneUptimeApiService { const instance = new Model(); return instance.tableName === tableName; } catch (error) { - MCPLogger.warn(`Error instantiating analytics model ${Model.name}: ${error}`); + MCPLogger.warn( + `Error instantiating analytics model ${Model.name}: ${error}`, + ); return false; } }); } if (!ModelClass) { - MCPLogger.warn(`Model class not found for ${tableName}, using empty select`); + MCPLogger.warn( + `Model class not found for ${tableName}, using empty select`, + ); return {}; } - MCPLogger.info(`Found ModelClass: ${ModelClass.name} for tableName: ${tableName}`); + MCPLogger.info( + `Found ModelClass: ${ModelClass.name} for tableName: ${tableName}`, + ); // Try to get raw table columns first (most reliable approach) try { const modelInstance = new ModelClass(); const tableColumns = getTableColumns(modelInstance); const columnNames = Object.keys(tableColumns); - - MCPLogger.info(`Raw table columns (${columnNames.length}): ${columnNames.slice(0, 10).join(', ')}`); - + + MCPLogger.info( + `Raw table columns (${columnNames.length}): ${columnNames.slice(0, 10).join(", ")}`, + ); + if (columnNames.length > 0) { // Get access control information to filter out restricted fields - const accessControlForColumns = modelInstance.getColumnAccessControlForAllColumns(); + const accessControlForColumns = + modelInstance.getColumnAccessControlForAllColumns(); const selectObject: JSONObject = {}; let filteredCount = 0; - + for (const columnName of columnNames) { const accessControl = accessControlForColumns[columnName]; - + // Include the field if: // 1. No access control defined (open access) // 2. Has read permissions that are not empty // 3. Read permissions don't only contain Permission.CurrentUser - if (!accessControl || - (accessControl.read && - accessControl.read.length > 0 && - !(accessControl.read.length === 1 && accessControl.read[0] === Permission.CurrentUser))) { + if ( + !accessControl || + (accessControl.read && + accessControl.read.length > 0 && + !( + accessControl.read.length === 1 && + accessControl.read[0] === Permission.CurrentUser + )) + ) { selectObject[columnName] = true; } else { filteredCount++; MCPLogger.info(`Filtered out restricted field: ${columnName}`); } } - - MCPLogger.info(`Generated select from table columns for ${tableName} with ${Object.keys(selectObject).length} fields (filtered out ${filteredCount} restricted fields)`); - + + MCPLogger.info( + `Generated select from table columns for ${tableName} with ${Object.keys(selectObject).length} fields (filtered out ${filteredCount} restricted fields)`, + ); + // Ensure we have at least some basic fields if (Object.keys(selectObject).length === 0) { - MCPLogger.warn(`All fields were filtered out, adding safe basic fields`); + MCPLogger.warn( + `All fields were filtered out, adding safe basic fields`, + ); selectObject["_id"] = true; selectObject["createdAt"] = true; selectObject["updatedAt"] = true; } - + return selectObject; } } catch (tableColumnError) { - MCPLogger.warn(`Failed to get table columns for ${tableName}: ${tableColumnError}`); + MCPLogger.warn( + `Failed to get table columns for ${tableName}: ${tableColumnError}`, + ); } // Fallback to schema approach if table columns fail let selectSchema: any; if (modelType === ModelType.Database) { - MCPLogger.info(`Generating select schema for database model: ${ModelClass.name}`); - selectSchema = ModelSchema.getSelectModelSchema({ modelType: ModelClass }); + MCPLogger.info( + `Generating select schema for database model: ${ModelClass.name}`, + ); + selectSchema = ModelSchema.getSelectModelSchema({ + modelType: ModelClass, + }); } else { - MCPLogger.info(`Generating schema for analytics model: ${ModelClass.name}`); + MCPLogger.info( + `Generating schema for analytics model: ${ModelClass.name}`, + ); // For analytics models, use the general model schema - selectSchema = AnalyticsModelSchema.getModelSchema({ modelType: ModelClass }); + selectSchema = AnalyticsModelSchema.getModelSchema({ + modelType: ModelClass, + }); } // Extract field names from the schema const selectObject: JSONObject = {}; const shape = selectSchema._def?.shape; - - MCPLogger.info(`Schema shape keys: ${shape ? Object.keys(shape).length : 0}`); - + + MCPLogger.info( + `Schema shape keys: ${shape ? Object.keys(shape).length : 0}`, + ); + if (shape) { const fieldNames = Object.keys(shape); - MCPLogger.info(`Available fields: ${fieldNames.slice(0, 10).join(', ')}${fieldNames.length > 10 ? '...' : ''}`); - + MCPLogger.info( + `Available fields: ${fieldNames.slice(0, 10).join(", ")}${fieldNames.length > 10 ? "..." : ""}`, + ); + for (const fieldName of fieldNames) { selectObject[fieldName] = true; } } - MCPLogger.info(`Generated select for ${tableName} with ${Object.keys(selectObject).length} fields`); - + MCPLogger.info( + `Generated select for ${tableName} with ${Object.keys(selectObject).length} fields`, + ); + // Force include some basic fields if select is empty if (Object.keys(selectObject).length === 0) { MCPLogger.warn(`No fields found, adding basic fields for ${tableName}`); - selectObject['_id'] = true; - selectObject['createdAt'] = true; - selectObject['updatedAt'] = true; + selectObject["_id"] = true; + selectObject["createdAt"] = true; + selectObject["updatedAt"] = true; } - + return selectObject; } catch (error) { MCPLogger.error(`Error generating select for ${tableName}: ${error}`); // Return some basic fields as fallback return { - '_id': true, - 'createdAt': true, - 'updatedAt': true + _id: true, + createdAt: true, + updatedAt: true, }; } } private static getHeaders(): Headers { const headers: Headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', + "Content-Type": "application/json", + Accept: "application/json", }; if (this.config.apiKey) { - headers['APIKey'] = this.config.apiKey; + headers["APIKey"] = this.config.apiKey; } return headers; @@ -337,15 +411,22 @@ export default class OneUptimeApiService { /** * Validate arguments for a specific operation */ - public static validateOperationArgs(operation: OneUptimeOperation, args: OneUptimeToolCallArgs): void { + public static validateOperationArgs( + operation: OneUptimeOperation, + args: OneUptimeToolCallArgs, + ): void { switch (operation) { case OneUptimeOperation.Create: // For create operations, we need at least one data field (excluding reserved fields) - const createDataFields = Object.keys(args).filter(key => - !['id', 'query', 'select', 'skip', 'limit', 'sort'].includes(key) - ); + const createDataFields = Object.keys(args).filter((key) => { + return !["id", "query", "select", "skip", "limit", "sort"].includes( + key, + ); + }); if (createDataFields.length === 0) { - throw new Error('At least one data field is required for create operation'); + throw new Error( + "At least one data field is required for create operation", + ); } break; case OneUptimeOperation.Read: @@ -356,11 +437,15 @@ export default class OneUptimeApiService { } if (operation === OneUptimeOperation.Update) { // For update operations, we need at least one data field (excluding reserved fields) - const updateDataFields = Object.keys(args).filter(key => - !['id', 'query', 'select', 'skip', 'limit', 'sort'].includes(key) - ); + const updateDataFields = Object.keys(args).filter((key) => { + return !["id", "query", "select", "skip", "limit", "sort"].includes( + key, + ); + }); if (updateDataFields.length === 0) { - throw new Error('At least one data field is required for update operation'); + throw new Error( + "At least one data field is required for update operation", + ); } } break; diff --git a/MCP/Tests/Utils/DynamicToolGenerator.test.ts b/MCP/Tests/Utils/DynamicToolGenerator.test.ts index 4cd72dcb18..28673767ce 100644 --- a/MCP/Tests/Utils/DynamicToolGenerator.test.ts +++ b/MCP/Tests/Utils/DynamicToolGenerator.test.ts @@ -7,92 +7,125 @@ describe("DynamicToolGenerator", () => { describe("zodToJsonSchema debugging", () => { test("should debug Zod schema structure and OpenAPI metadata", () => { // Generate the create schema for Incident - const createSchema = ModelSchema.getCreateModelSchema({ modelType: Incident }); - + const createSchema = ModelSchema.getCreateModelSchema({ + modelType: Incident, + }); + console.log("=== Schema Structure Debug ==="); console.log("Schema type:", typeof createSchema); console.log("Schema constructor:", createSchema.constructor.name); console.log("Schema _def:", JSON.stringify(createSchema._def, null, 2)); - + // Check if shape exists and is a function const shape = (createSchema as any)._def?.shape; - console.log("Shape exists:", !!shape); + console.log("Shape exists:", Boolean(shape)); console.log("Shape type:", typeof shape); - - if (shape && typeof shape === 'function') { + + if (shape && typeof shape === "function") { const shapeResult = shape(); console.log("Shape result type:", typeof shapeResult); console.log("Shape result keys:", Object.keys(shapeResult)); - + // Look at a few fields to understand the structure const fields = Object.entries(shapeResult).slice(0, 3); - + for (const [key, value] of fields) { console.log(`\n=== Field: ${key} ===`); console.log("Field type:", typeof value); console.log("Field constructor:", (value as any).constructor.name); - console.log("Field _def:", JSON.stringify((value as any)._def, null, 2)); - + console.log( + "Field _def:", + JSON.stringify((value as any)._def, null, 2), + ); + // Check different possible locations for OpenAPI metadata console.log("_def.openapi:", (value as any)._def?.openapi); console.log("_def.description:", (value as any)._def?.description); console.log("_def.meta:", (value as any)._def?.meta); console.log("openapi property direct:", (value as any).openapi); - + // Check if it's a ZodOptional and look at the inner type - if ((value as any)._def?.typeName === 'ZodOptional') { - console.log("Inner type _def:", JSON.stringify((value as any)._def?.innerType?._def, null, 2)); - console.log("Inner type openapi:", (value as any)._def?.innerType?._def?.openapi); + if ((value as any)._def?.typeName === "ZodOptional") { + console.log( + "Inner type _def:", + JSON.stringify((value as any)._def?.innerType?._def, null, 2), + ); + console.log( + "Inner type openapi:", + (value as any)._def?.innerType?._def?.openapi, + ); } } } - + // Test the current zodToJsonSchema method to see what it produces console.log("\n=== Current zodToJsonSchema Output ==="); - const jsonSchema = (DynamicToolGenerator as any).zodToJsonSchema(createSchema); - console.log("Generated JSON Schema:", JSON.stringify(jsonSchema, null, 2)); + const jsonSchema = (DynamicToolGenerator as any).zodToJsonSchema( + createSchema, + ); + console.log( + "Generated JSON Schema:", + JSON.stringify(jsonSchema, null, 2), + ); }); test("should generate tools for Incident model", () => { const incident = new Incident(); - const tools = DynamicToolGenerator.generateToolsForDatabaseModel(incident, Incident); - + const tools = DynamicToolGenerator.generateToolsForDatabaseModel( + incident, + Incident, + ); + console.log("\n=== Generated Tools ==="); console.log("Number of tools:", tools.tools.length); - + // Check the create tool specifically - const createTool = tools.tools.find(tool => tool.operation === OneUptimeOperation.Create); + const createTool = tools.tools.find((tool) => {return tool.operation === OneUptimeOperation.Create}); if (createTool) { - console.log("Create tool input schema properties:", Object.keys(createTool.inputSchema.properties)); - console.log("Sample properties:", JSON.stringify( - Object.fromEntries(Object.entries(createTool.inputSchema.properties).slice(0, 3)), - null, - 2 - )); + console.log( + "Create tool input schema properties:", + Object.keys(createTool.inputSchema.properties), + ); + console.log( + "Sample properties:", + JSON.stringify( + Object.fromEntries( + Object.entries(createTool.inputSchema.properties).slice(0, 3), + ), + null, + 2, + ), + ); } }); test("should extract proper OpenAPI metadata instead of fallback", () => { - const createSchema = ModelSchema.getCreateModelSchema({ modelType: Incident }); - const jsonSchema = (DynamicToolGenerator as any).zodToJsonSchema(createSchema); - + const createSchema = ModelSchema.getCreateModelSchema({ + modelType: Incident, + }); + const jsonSchema = (DynamicToolGenerator as any).zodToJsonSchema( + createSchema, + ); + // Check that we're extracting proper types instead of fallback expect(jsonSchema.properties.title.type).toBe("string"); expect(jsonSchema.properties.title.description).not.toBe("title field"); // Should not be fallback - expect(jsonSchema.properties.title.description).toContain("Title of this incident"); // Should have real description + expect(jsonSchema.properties.title.description).toContain( + "Title of this incident", + ); // Should have real description expect(jsonSchema.properties.title.example).toBeTruthy(); // Should have example - + // Check boolean field expect(jsonSchema.properties.isVisibleOnStatusPage.type).toBe("boolean"); expect(jsonSchema.properties.isVisibleOnStatusPage.default).toBe(true); - + // Check UUID format field expect(jsonSchema.properties.projectId.format).toBe("uuid"); expect(jsonSchema.properties.projectId.type).toBe("string"); - + // Check array field expect(jsonSchema.properties.monitors.type).toBe("array"); - + // Check that required fields are properly identified expect(jsonSchema.required).toContain("title"); expect(jsonSchema.required).toContain("incidentSeverityId"); @@ -103,15 +136,26 @@ describe("DynamicToolGenerator", () => { test("should properly sanitize tool names for complex model names", () => { // Test the sanitizeToolName function directly const testCases = [ - { input: "UsersOnCallDutyEscalationRule", expected: "users_on_call_duty_escalation_rule" }, - { input: "User's On-Call Duty Escalation Rule", expected: "user_s_on_call_duty_escalation_rule" }, - { input: "Users On-Call Duty Escalation Rule", expected: "users_on_call_duty_escalation_rule" }, + { + input: "UsersOnCallDutyEscalationRule", + expected: "users_on_call_duty_escalation_rule", + }, + { + input: "User's On-Call Duty Escalation Rule", + expected: "user_s_on_call_duty_escalation_rule", + }, + { + input: "Users On-Call Duty Escalation Rule", + expected: "users_on_call_duty_escalation_rule", + }, { input: "MonitorGroup", expected: "monitor_group" }, - { input: "StatusPageSubscriber", expected: "status_page_subscriber" } + { input: "StatusPageSubscriber", expected: "status_page_subscriber" }, ]; - + for (const testCase of testCases) { - const result = (DynamicToolGenerator as any).sanitizeToolName(testCase.input); + const result = (DynamicToolGenerator as any).sanitizeToolName( + testCase.input, + ); console.log(`Input: "${testCase.input}" -> Output: "${result}"`); expect(result).toBe(testCase.expected); } @@ -119,34 +163,37 @@ describe("DynamicToolGenerator", () => { test("should generate proper tool names for OnCallDutyPolicyEscalationRule model", () => { // Test the actual model that was mentioned in the issue - const onCallDutyModel = new (require("Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule").default)(); + const onCallDutyModel = + new (require("Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule").default)(); const tools = DynamicToolGenerator.generateToolsForDatabaseModel( - onCallDutyModel, - require("Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule").default + onCallDutyModel, + require("Common/Models/DatabaseModels/OnCallDutyPolicyEscalationRule") + .default, ); - + console.log("OnCallDutyPolicyEscalationRule model info:"); console.log("- tableName:", onCallDutyModel.tableName); console.log("- singularName:", onCallDutyModel.singularName); console.log("- pluralName:", onCallDutyModel.pluralName); - + console.log("\nGenerated tool names:"); - tools.tools.forEach(tool => { + tools.tools.forEach((tool) => { console.log(`- ${tool.operation}: ${tool.name}`); }); - + // Check that tool names are properly formatted - const createTool = tools.tools.find(tool => tool.operation === OneUptimeOperation.Create); - const listTool = tools.tools.find(tool => tool.operation === OneUptimeOperation.List); - + const createTool = tools.tools.find((tool) => {return tool.operation === OneUptimeOperation.Create}); + const listTool = tools.tools.find((tool) => {return tool.operation === OneUptimeOperation.List}); + ); + // Should be create_escalation_rule not create_escalation_rule_s or something weird expect(createTool?.name).toMatch(/^create_[a-z_]+$/); expect(listTool?.name).toMatch(/^list_[a-z_]+$/); - + // Should not contain invalid patterns like "_s_" or double underscores - tools.tools.forEach(tool => { - expect(tool.name).not.toContain('_s_'); - expect(tool.name).not.toContain('__'); + tools.tools.forEach((tool) => { + expect(tool.name).not.toContain("_s_"); + expect(tool.name).not.toContain("__"); expect(tool.name).toMatch(/^[a-z0-9_]+$/); }); }); @@ -155,32 +202,38 @@ describe("DynamicToolGenerator", () => { // Test the cleanDescription method directly const testCases = [ { - input: "Should this incident be visible on the status page?. Permissions - Create: [Project Owner, Project Admin, Project Member, Create Incident], Read: [Project Owner, Project Admin, Project Member, Read Incident], Update: [Project Owner, Project Admin, Project Member, Edit Incident]", - expected: "Should this incident be visible on the status page?" - }, { - input: "Title of this incident. Permissions - Create: [Project Owner, Project Admin, Project Member, Create Incident], Read: [Project Owner, Project Admin, Project Member, Read Incident], Update: [Project Owner, Project Admin, Project Member, Edit Incident]", - expected: "Title of this incident." - }, - { - input: "Simple description without permissions", - expected: "Simple description without permissions" + input: + "Should this incident be visible on the status page?. Permissions - Create: [Project Owner, Project Admin, Project Member, Create Incident], Read: [Project Owner, Project Admin, Project Member, Read Incident], Update: [Project Owner, Project Admin, Project Member, Edit Incident]", + expected: "Should this incident be visible on the status page?", }, { - input: "Description ending with period. Permissions - Create: [Some permissions here]", - expected: "Description ending with period." + input: + "Title of this incident. Permissions - Create: [Project Owner, Project Admin, Project Member, Create Incident], Read: [Project Owner, Project Admin, Project Member, Read Incident], Update: [Project Owner, Project Admin, Project Member, Edit Incident]", + expected: "Title of this incident.", + }, + { + input: "Simple description without permissions", + expected: "Simple description without permissions", + }, + { + input: + "Description ending with period. Permissions - Create: [Some permissions here]", + expected: "Description ending with period.", }, { input: "Permissions - Create: [Only permissions, no description]", - expected: "Permissions - Create: [Only permissions, no description]" - } + expected: "Permissions - Create: [Only permissions, no description]", + }, ]; - + for (const testCase of testCases) { - const result = (DynamicToolGenerator as any).cleanDescription(testCase.input); + const result = (DynamicToolGenerator as any).cleanDescription( + testCase.input, + ); console.log(`Input: "${testCase.input}"`); console.log(`Expected: "${testCase.expected}"`); console.log(`Result: "${result}"`); - console.log('---'); + console.log("---"); expect(result).toBe(testCase.expected); } }); diff --git a/MCP/Types/OneUptimeOperation.ts b/MCP/Types/OneUptimeOperation.ts index 63f1eeaea5..b940544cfa 100644 --- a/MCP/Types/OneUptimeOperation.ts +++ b/MCP/Types/OneUptimeOperation.ts @@ -1,6 +1,6 @@ export enum OneUptimeOperation { Create = "create", - Read = "read", + Read = "read", List = "list", Update = "update", Delete = "delete", diff --git a/MCP/Utils/DynamicToolGenerator.ts b/MCP/Utils/DynamicToolGenerator.ts index 02304aa7b7..f6b8194250 100644 --- a/MCP/Utils/DynamicToolGenerator.ts +++ b/MCP/Utils/DynamicToolGenerator.ts @@ -6,33 +6,39 @@ import OneUptimeOperation from "../Types/OneUptimeOperation"; import ModelType from "../Types/ModelType"; import { McpToolInfo, ModelToolsResult } from "../Types/McpTypes"; import { ModelSchema, ModelSchemaType } from "Common/Utils/Schema/ModelSchema"; -import { AnalyticsModelSchema, AnalyticsModelSchemaType } from "Common/Utils/Schema/AnalyticsModelSchema"; +import { + AnalyticsModelSchema, + AnalyticsModelSchemaType, +} from "Common/Utils/Schema/AnalyticsModelSchema"; import MCPLogger from "./MCPLogger"; export default class DynamicToolGenerator { - /** * Sanitize a name to be valid for MCP tool names * MCP tool names can only contain [a-z0-9_-] */ private static sanitizeToolName(name: string): string { - return name - // First convert camelCase to snake_case by adding underscores before uppercase letters - .replace(/([a-z])([A-Z])/g, '$1_$2') - .toLowerCase() - // Replace any non-alphanumeric characters (including spaces, hyphens) with underscores - .replace(/[^a-z0-9]/g, '_') - // Replace multiple consecutive underscores with single underscore - .replace(/_+/g, '_') - // Remove leading/trailing underscores - .replace(/^_|_$/g, ''); + return ( + name + // First convert camelCase to snake_case by adding underscores before uppercase letters + .replace(/([a-z])([A-Z])/g, "$1_$2") + .toLowerCase() + // Replace any non-alphanumeric characters (including spaces, hyphens) with underscores + .replace(/[^a-z0-9]/g, "_") + // Replace multiple consecutive underscores with single underscore + .replace(/_+/g, "_") + // Remove leading/trailing underscores + .replace(/^_|_$/g, "") + ); } /** * Convert a Zod schema to JSON Schema format for MCP tools * This is a simple converter that extracts the OpenAPI specification from Zod schemas */ - private static zodToJsonSchema(zodSchema: ModelSchemaType | AnalyticsModelSchemaType): any { + private static zodToJsonSchema( + zodSchema: ModelSchemaType | AnalyticsModelSchemaType, + ): any { try { // The Zod schemas in this project are extended with OpenAPI metadata // We can extract the shape and create a basic JSON schema @@ -42,7 +48,7 @@ export default class DynamicToolGenerator { return { type: "object", properties: {}, - additionalProperties: false + additionalProperties: false, }; } @@ -51,46 +57,55 @@ export default class DynamicToolGenerator { for (const [key, value] of Object.entries(shape())) { const zodField = value as any; - + // Handle ZodOptional fields by looking at the inner type let actualField = zodField; let isOptional = false; - - if (zodField._def?.typeName === 'ZodOptional') { + + if (zodField._def?.typeName === "ZodOptional") { actualField = zodField._def.innerType; isOptional = true; } - + // Extract OpenAPI metadata - it's stored in _def.openapi.metadata - const openApiMetadata = actualField._def?.openapi?.metadata || zodField._def?.openapi?.metadata; - + const openApiMetadata = + actualField._def?.openapi?.metadata || + zodField._def?.openapi?.metadata; + // Clean up description by removing permission information - const rawDescription = zodField._def?.description || openApiMetadata?.description || `${key} field`; + const rawDescription = + zodField._def?.description || + openApiMetadata?.description || + `${key} field`; const cleanDescription = this.cleanDescription(rawDescription); - + if (openApiMetadata) { const fieldSchema: any = { type: openApiMetadata.type || "string", description: cleanDescription, - ...(openApiMetadata.example !== undefined && { example: openApiMetadata.example }), + ...(openApiMetadata.example !== undefined && { + example: openApiMetadata.example, + }), ...(openApiMetadata.format && { format: openApiMetadata.format }), - ...(openApiMetadata.default !== undefined && { default: openApiMetadata.default }) + ...(openApiMetadata.default !== undefined && { + default: openApiMetadata.default, + }), }; - + // Handle array types - ensure they have proper items schema for MCP validation if (openApiMetadata?.type === "array") { fieldSchema.items = openApiMetadata.items || { type: "string", - description: `${key} item` + description: `${key} item`, }; } - + properties[key] = fieldSchema; } else { // Fallback for fields without OpenAPI metadata properties[key] = { type: "string", - description: cleanDescription + description: cleanDescription, }; } @@ -104,23 +119,23 @@ export default class DynamicToolGenerator { type: "object", properties, required: required.length > 0 ? required : undefined, - additionalProperties: false + additionalProperties: false, }; } catch (error) { return { type: "object", properties: {}, - additionalProperties: false + additionalProperties: false, }; } } - + /** * Generate all MCP tools for all OneUptime models */ public static generateAllTools(): McpToolInfo[] { const allTools: McpToolInfo[] = []; - + // Generate tools for Database Models for (const ModelClass of DatabaseModels) { try { @@ -128,22 +143,28 @@ export default class DynamicToolGenerator { const tools = this.generateToolsForDatabaseModel(model, ModelClass); allTools.push(...tools.tools); } catch (error) { - MCPLogger.error(`Error generating tools for database model ${ModelClass.name}: ${error}`); + MCPLogger.error( + `Error generating tools for database model ${ModelClass.name}: ${error}`, + ); } } - - // Generate tools for Analytics Models + + // Generate tools for Analytics Models for (const ModelClass of AnalyticsModels) { try { const model: AnalyticsBaseModel = new ModelClass(); const tools = this.generateToolsForAnalyticsModel(model, ModelClass); allTools.push(...tools.tools); } catch (error) { - MCPLogger.error(`Error generating tools for analytics model ${ModelClass.name}: ${error}`); + MCPLogger.error( + `Error generating tools for analytics model ${ModelClass.name}: ${error}`, + ); } } - - MCPLogger.info(`Generated ${allTools.length} MCP tools for OneUptime models`); + + MCPLogger.info( + `Generated ${allTools.length} MCP tools for OneUptime models`, + ); return allTools; } @@ -152,7 +173,7 @@ export default class DynamicToolGenerator { */ public static generateToolsForDatabaseModel( model: DatabaseBaseModel, - ModelClass: { new (): DatabaseBaseModel } + ModelClass: { new (): DatabaseBaseModel }, ): ModelToolsResult { const tools: McpToolInfo[] = []; const modelName = model.tableName || ModelClass.name; @@ -169,16 +190,24 @@ export default class DynamicToolGenerator { singularName, pluralName, modelType: ModelType.Database, - ...(apiPath && { apiPath }) - } + ...(apiPath && { apiPath }), + }, }; } // Generate schemas using ModelSchema - const createSchema: ModelSchemaType = ModelSchema.getCreateModelSchema({ modelType: ModelClass }); - const updateSchema: ModelSchemaType = ModelSchema.getUpdateModelSchema({ modelType: ModelClass}); - const querySchema: ModelSchemaType = ModelSchema.getQueryModelSchema({ modelType: ModelClass}); - const sortSchema: ModelSchemaType = ModelSchema.getSortModelSchema({ modelType: ModelClass }); + const createSchema: ModelSchemaType = ModelSchema.getCreateModelSchema({ + modelType: ModelClass, + }); + const updateSchema: ModelSchemaType = ModelSchema.getUpdateModelSchema({ + modelType: ModelClass, + }); + const querySchema: ModelSchemaType = ModelSchema.getQueryModelSchema({ + modelType: ModelClass, + }); + const sortSchema: ModelSchemaType = ModelSchema.getSortModelSchema({ + modelType: ModelClass, + }); // CREATE Tool const createSchemaProperties = this.zodToJsonSchema(createSchema); @@ -189,7 +218,7 @@ export default class DynamicToolGenerator { type: "object", properties: createSchemaProperties.properties || {}, required: createSchemaProperties.required || [], - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Create, @@ -197,10 +226,10 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); - // READ Tool + // READ Tool tools.push({ name: `get_${this.sanitizeToolName(singularName)}`, description: `Retrieve a single ${singularName} by ID from OneUptime`, @@ -210,10 +239,10 @@ export default class DynamicToolGenerator { id: { type: "string", description: `ID of the ${singularName} to retrieve`, - } + }, }, required: ["id"], - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Read, @@ -221,7 +250,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); // LIST Tool @@ -234,15 +263,17 @@ export default class DynamicToolGenerator { query: this.zodToJsonSchema(querySchema), skip: { type: "number", - description: "Number of records to skip. This can be used for pagination.", + description: + "Number of records to skip. This can be used for pagination.", }, limit: { type: "number", - description: "Maximum number of records to return. This can be used for pagination. Maximum value is 100.", + description: + "Maximum number of records to return. This can be used for pagination. Maximum value is 100.", }, - sort: this.zodToJsonSchema(sortSchema) + sort: this.zodToJsonSchema(sortSchema), }, - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.List, @@ -250,7 +281,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); // UPDATE Tool @@ -265,10 +296,10 @@ export default class DynamicToolGenerator { type: "string", description: `ID of the ${singularName} to update`, }, - ...updateSchemaProperties.properties || {} + ...(updateSchemaProperties.properties || {}), }, required: ["id"], - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Update, @@ -276,7 +307,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); // DELETE Tool @@ -289,10 +320,10 @@ export default class DynamicToolGenerator { id: { type: "string", description: `ID of the ${singularName} to delete`, - } + }, }, required: ["id"], - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Delete, @@ -300,7 +331,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); // COUNT Tool @@ -310,9 +341,9 @@ export default class DynamicToolGenerator { inputSchema: { type: "object", properties: { - query: this.zodToJsonSchema(querySchema) + query: this.zodToJsonSchema(querySchema), }, - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Count, @@ -320,7 +351,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); return { @@ -330,8 +361,8 @@ export default class DynamicToolGenerator { singularName, pluralName, modelType: ModelType.Database, - apiPath - } + apiPath, + }, }; } @@ -340,7 +371,7 @@ export default class DynamicToolGenerator { */ public static generateToolsForAnalyticsModel( model: AnalyticsBaseModel, - ModelClass: { new (): AnalyticsBaseModel } + ModelClass: { new (): AnalyticsBaseModel }, ): ModelToolsResult { const tools: McpToolInfo[] = []; const modelName = model.tableName || ModelClass.name; @@ -357,16 +388,29 @@ export default class DynamicToolGenerator { singularName, pluralName, modelType: ModelType.Analytics, - apiPath - } + apiPath, + }, }; } // Generate schemas using AnalyticsModelSchema - const createSchema: AnalyticsModelSchemaType = AnalyticsModelSchema.getCreateModelSchema({ modelType: ModelClass, disableOpenApiSchema: true }); - const querySchema: AnalyticsModelSchemaType = AnalyticsModelSchema.getQueryModelSchema({ modelType: ModelClass, disableOpenApiSchema: true }); - const selectSchema: AnalyticsModelSchemaType = AnalyticsModelSchema.getSelectModelSchema({ modelType: ModelClass }); - const sortSchema: AnalyticsModelSchemaType = AnalyticsModelSchema.getSortModelSchema({ modelType: ModelClass, disableOpenApiSchema: true }); + const createSchema: AnalyticsModelSchemaType = + AnalyticsModelSchema.getCreateModelSchema({ + modelType: ModelClass, + disableOpenApiSchema: true, + }); + const querySchema: AnalyticsModelSchemaType = + AnalyticsModelSchema.getQueryModelSchema({ + modelType: ModelClass, + disableOpenApiSchema: true, + }); + const selectSchema: AnalyticsModelSchemaType = + AnalyticsModelSchema.getSelectModelSchema({ modelType: ModelClass }); + const sortSchema: AnalyticsModelSchemaType = + AnalyticsModelSchema.getSortModelSchema({ + modelType: ModelClass, + disableOpenApiSchema: true, + }); // CREATE Tool for Analytics const analyticsCreateSchemaProperties = this.zodToJsonSchema(createSchema); @@ -377,7 +421,7 @@ export default class DynamicToolGenerator { type: "object", properties: analyticsCreateSchemaProperties.properties || {}, required: analyticsCreateSchemaProperties.required || [], - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Create, @@ -385,7 +429,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); // LIST Tool for Analytics (most common operation) @@ -405,9 +449,9 @@ export default class DynamicToolGenerator { type: "number", description: "Maximum number of records to return", }, - sort: this.zodToJsonSchema(sortSchema) + sort: this.zodToJsonSchema(sortSchema), }, - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.List, @@ -415,7 +459,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); // COUNT Tool for Analytics @@ -425,9 +469,9 @@ export default class DynamicToolGenerator { inputSchema: { type: "object", properties: { - query: this.zodToJsonSchema(querySchema) + query: this.zodToJsonSchema(querySchema), }, - additionalProperties: false + additionalProperties: false, }, modelName, operation: OneUptimeOperation.Count, @@ -435,7 +479,7 @@ export default class DynamicToolGenerator { singularName, pluralName, tableName: modelName, - apiPath + apiPath, }); return { @@ -445,8 +489,8 @@ export default class DynamicToolGenerator { singularName, pluralName, modelType: ModelType.Analytics, - apiPath - } + apiPath, + }, }; } @@ -457,32 +501,41 @@ export default class DynamicToolGenerator { if (!description) { return description; } - + // Remove everything after "Permissions -" (including the word "Permissions") - const permissionsIndex = description.indexOf('. Permissions -'); + const permissionsIndex = description.indexOf(". Permissions -"); if (permissionsIndex !== -1) { // Get the text before ". Permissions -", and add back the period if it makes sense const beforeText = description.substring(0, permissionsIndex); // Add period back if the text doesn't already end with punctuation - if (beforeText && !beforeText.endsWith('.') && !beforeText.endsWith('!') && !beforeText.endsWith('?')) { - return beforeText + '.'; + if ( + beforeText && + !beforeText.endsWith(".") && + !beforeText.endsWith("!") && + !beforeText.endsWith("?") + ) { + return beforeText + "."; } return beforeText; } - + // Also handle cases where it starts with "Permissions -" without a preceding sentence - const permissionsStartIndex = description.indexOf('Permissions -'); + const permissionsStartIndex = description.indexOf("Permissions -"); if (permissionsStartIndex !== -1) { - const beforePermissions = description.substring(0, permissionsStartIndex).trim(); + const beforePermissions = description + .substring(0, permissionsStartIndex) + .trim(); // If there's meaningful content before "Permissions", return that if (beforePermissions && beforePermissions.length > 0) { // Add a period if it doesn't end with punctuation - return beforePermissions.endsWith('.') || beforePermissions.endsWith('!') || beforePermissions.endsWith('?') - ? beforePermissions - : beforePermissions + '.'; + return beforePermissions.endsWith(".") || + beforePermissions.endsWith("!") || + beforePermissions.endsWith("?") + ? beforePermissions + : beforePermissions + "."; } } - + return description; } } diff --git a/MCP/__tests__/server.test.ts b/MCP/__tests__/server.test.ts index 4e7c3f3515..551608f9b7 100644 --- a/MCP/__tests__/server.test.ts +++ b/MCP/__tests__/server.test.ts @@ -1,16 +1,16 @@ -import { describe, it, expect } from '@jest/globals'; +import { describe, it, expect } from "@jest/globals"; -describe('MCP Hello World Server', () => { - it('should have basic structure', () => { +describe("MCP Hello World Server", () => { + it("should have basic structure", () => { // Basic test to ensure the test setup works expect(true).toBe(true); }); - it('should export required tools', () => { + it("should export required tools", () => { // Test for tool definitions - const expectedTools = ['hello', 'get_time', 'echo']; - expect(expectedTools).toContain('hello'); - expect(expectedTools).toContain('get_time'); - expect(expectedTools).toContain('echo'); + const expectedTools = ["hello", "get_time", "echo"]; + expect(expectedTools).toContain("hello"); + expect(expectedTools).toContain("get_time"); + expect(expectedTools).toContain("echo"); }); });