diff --git a/MCP/Index.ts b/MCP/Index.ts index 2327659c66..870b72e3c1 100755 --- a/MCP/Index.ts +++ b/MCP/Index.ts @@ -4,6 +4,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { CallToolRequestSchema, + CallToolRequest, ErrorCode, ListToolsRequestSchema, McpError, @@ -106,8 +107,7 @@ function setupHandlers(): void { // Handle tool calls mcpServer.setRequestHandler( CallToolRequestSchema, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (request: any) => { + async (request: CallToolRequest) => { const { name, arguments: args } = request.params; try { diff --git a/MCP/Services/OneUptimeApiService.ts b/MCP/Services/OneUptimeApiService.ts index d84e403d0c..0c15b0e76e 100644 --- a/MCP/Services/OneUptimeApiService.ts +++ b/MCP/Services/OneUptimeApiService.ts @@ -8,7 +8,7 @@ import Route from "Common/Types/API/Route"; import Headers from "Common/Types/API/Headers"; import HTTPResponse from "Common/Types/API/HTTPResponse"; import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse"; -import { JSONObject } from "Common/Types/JSON"; +import { JSONObject, JSONValue } from "Common/Types/JSON"; import DatabaseModels from "Common/Models/DatabaseModels/Index"; import AnalyticsModels from "Common/Models/AnalyticsModels/Index"; import { ModelSchema } from "Common/Utils/Schema/ModelSchema"; @@ -17,12 +17,43 @@ import { getTableColumns } from "Common/Types/Database/TableColumn"; import Permission from "Common/Types/Permission"; import Protocol from "Common/Types/API/Protocol"; import Hostname from "Common/Types/API/Hostname"; +import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel"; +import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel"; export interface OneUptimeApiConfig { url: string; apiKey: string; } +// Type for model constructor +type ModelConstructor = new () => T; + +// Type for model class with table name +interface ModelWithTableName { + tableName: string; + getColumnAccessControlForAllColumns?: () => Record< + string, + ColumnAccessControl + >; +} + +// Type for column access control +interface ColumnAccessControl { + read?: Permission[]; + create?: Permission[]; + update?: Permission[]; +} + +// Type for table columns +type TableColumns = Record; + +// Type for Zod schema shape (shape can be an object or a function returning an object) +interface ZodSchemaWithShape { + _def?: { + shape?: Record | (() => Record); + }; +} + export default class OneUptimeApiService { private static api: API; private static config: OneUptimeApiConfig; @@ -60,7 +91,7 @@ export default class OneUptimeApiService { modelType: ModelType, apiPath: string, args: OneUptimeToolCallArgs, - ): Promise { + ): Promise { if (!this.api) { throw new Error( "OneUptime API Service not initialized. Please call initialize() first.", @@ -69,9 +100,9 @@ export default class OneUptimeApiService { this.validateOperationArgs(operation, args); - const route: any = this.buildApiRoute(apiPath, operation, args.id); - const headers: any = this.getHeaders(); - const data: any = this.getRequestData( + const route: Route = this.buildApiRoute(apiPath, operation, args.id); + const headers: Headers = this.getHeaders(); + const data: JSONObject | undefined = this.getRequestData( operation, args, tableName, @@ -83,35 +114,35 @@ export default class OneUptimeApiService { ); try { - let response: HTTPResponse | HTTPErrorResponse; + let response: HTTPResponse | HTTPErrorResponse; // Create a direct URL to avoid base route accumulation const url: URL = new URL(this.api.protocol, this.api.hostname, route); + // Build request options, only including data if it's defined + const baseOptions: { url: URL; headers: Headers } = { + url: url, + headers: headers, + }; + switch (operation) { case OneUptimeOperation.Create: case OneUptimeOperation.Count: case OneUptimeOperation.List: case OneUptimeOperation.Read: - response = await API.post({ - url: url, - data: data, - headers: headers, - }); + response = await API.post( + data ? { ...baseOptions, data: data } : baseOptions, + ); break; case OneUptimeOperation.Update: - response = await API.put({ - url: url, - data: data, - headers: headers, - }); + response = await API.put( + data ? { ...baseOptions, data: data } : baseOptions, + ); break; case OneUptimeOperation.Delete: - response = await API.delete({ - url: url, - data: data, - headers: headers, - }); + response = await API.delete( + data ? { ...baseOptions, data: data } : baseOptions, + ); break; default: throw new Error(`Unsupported operation: ${operation}`); @@ -190,7 +221,7 @@ export default class OneUptimeApiService { if ( !["id", "query", "select", "skip", "limit", "sort"].includes(key) ) { - createData[key] = value; + createData[key] = value as JSONValue; } } return { data: createData } as JSONObject; @@ -202,14 +233,14 @@ export default class OneUptimeApiService { if ( !["id", "query", "select", "skip", "limit", "sort"].includes(key) ) { - updateData[key] = value; + updateData[key] = value as JSONValue; } } return { data: updateData } as JSONObject; } case OneUptimeOperation.List: case OneUptimeOperation.Count: { - const generatedSelect: any = + const generatedSelect: JSONObject = args.select || this.generateAllFieldsSelect(tableName, modelType); const requestData: JSONObject = { query: args.query || {}, @@ -225,7 +256,7 @@ export default class OneUptimeApiService { return requestData; } case OneUptimeOperation.Read: { - const readSelect: any = + const readSelect: JSONObject = args.select || this.generateAllFieldsSelect(tableName, modelType); const readRequestData: JSONObject = { select: readSelect, @@ -254,37 +285,50 @@ export default class OneUptimeApiService { ); try { - let ModelClass: any = null; + let ModelClass: + | ModelConstructor + | ModelConstructor + | null = null; // Find the model class by table name if (modelType === ModelType.Database) { MCPLogger.info(`Searching DatabaseModels for tableName: ${tableName}`); - ModelClass = DatabaseModels.find((Model: any) => { - try { - const instance: any = new Model(); - const instanceTableName: string = instance.tableName; - MCPLogger.info( - `Checking model ${Model.name} with tableName: ${instanceTableName}`, - ); - return instanceTableName === tableName; - } catch (error) { - MCPLogger.warn(`Error instantiating model ${Model.name}: ${error}`); - return false; - } - }); + ModelClass = + (DatabaseModels.find( + (Model: ModelConstructor): boolean => { + try { + const instance: ModelWithTableName = + new Model() as unknown as ModelWithTableName; + const instanceTableName: string = instance.tableName; + MCPLogger.info( + `Checking model ${Model.name} with tableName: ${instanceTableName}`, + ); + return instanceTableName === tableName; + } catch (error) { + MCPLogger.warn( + `Error instantiating model ${Model.name}: ${error}`, + ); + return false; + } + }, + ) as ModelConstructor | undefined) || null; } else if (modelType === ModelType.Analytics) { MCPLogger.info(`Searching AnalyticsModels for tableName: ${tableName}`); - ModelClass = AnalyticsModels.find((Model: any) => { - try { - const instance: any = new Model(); - return instance.tableName === tableName; - } catch (error) { - MCPLogger.warn( - `Error instantiating analytics model ${Model.name}: ${error}`, - ); - return false; - } - }); + ModelClass = + (AnalyticsModels.find( + (Model: ModelConstructor): boolean => { + try { + const instance: ModelWithTableName = + new Model() as unknown as ModelWithTableName; + return instance.tableName === tableName; + } catch (error) { + MCPLogger.warn( + `Error instantiating analytics model ${Model.name}: ${error}`, + ); + return false; + } + }, + ) as ModelConstructor | undefined) || null; } if (!ModelClass) { @@ -300,8 +344,11 @@ export default class OneUptimeApiService { // Try to get raw table columns first (most reliable approach) try { - const modelInstance: any = new ModelClass(); - const tableColumns: any = getTableColumns(modelInstance); + const modelInstance: ModelWithTableName = + new ModelClass() as unknown as ModelWithTableName; + const tableColumns: TableColumns = getTableColumns( + modelInstance as BaseModel, + ); const columnNames: string[] = Object.keys(tableColumns); MCPLogger.info( @@ -310,13 +357,16 @@ export default class OneUptimeApiService { if (columnNames.length > 0) { // Get access control information to filter out restricted fields - const accessControlForColumns: any = - modelInstance.getColumnAccessControlForAllColumns(); + const accessControlForColumns: Record = + modelInstance.getColumnAccessControlForAllColumns + ? modelInstance.getColumnAccessControlForAllColumns() + : {}; const selectObject: JSONObject = {}; let filteredCount: number = 0; for (const columnName of columnNames) { - const accessControl: any = accessControlForColumns[columnName]; + const accessControl: ColumnAccessControl | undefined = + accessControlForColumns[columnName]; /* * Include the field if: @@ -363,27 +413,32 @@ export default class OneUptimeApiService { } // Fallback to schema approach if table columns fail - let selectSchema: any; + let selectSchema: ZodSchemaWithShape; if (modelType === ModelType.Database) { MCPLogger.info( `Generating select schema for database model: ${ModelClass.name}`, ); selectSchema = ModelSchema.getSelectModelSchema({ - modelType: ModelClass, - }); + modelType: ModelClass as ModelConstructor, + }) as ZodSchemaWithShape; } else { MCPLogger.info( `Generating schema for analytics model: ${ModelClass.name}`, ); // For analytics models, use the general model schema selectSchema = AnalyticsModelSchema.getModelSchema({ - modelType: ModelClass, - }); + modelType: ModelClass as ModelConstructor, + }) as ZodSchemaWithShape; } // Extract field names from the schema const selectObject: JSONObject = {}; - const shape: any = selectSchema._def?.shape; + const rawShape: Record | (() => Record) | undefined = + selectSchema._def?.shape; + + // Handle both function and object shapes + const shape: Record | undefined = + typeof rawShape === 'function' ? rawShape() : rawShape; MCPLogger.info( `Schema shape keys: ${shape ? Object.keys(shape).length : 0}`, diff --git a/MCP/Types/McpTypes.ts b/MCP/Types/McpTypes.ts index 3088045bb2..4c689ae1c3 100644 --- a/MCP/Types/McpTypes.ts +++ b/MCP/Types/McpTypes.ts @@ -1,10 +1,36 @@ import OneUptimeOperation from "./OneUptimeOperation"; import ModelType from "./ModelType"; +import { JSONObject } from "Common/Types/JSON"; + +// JSON Schema type for MCP tool input schemas +export interface JSONSchemaProperty { + type: string; + description?: string; + enum?: Array; + items?: JSONSchemaProperty; + properties?: Record; + required?: string[]; + default?: unknown; + format?: string; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; +} + +export interface JSONSchema { + type: string; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + description?: string; +} export interface McpToolInfo { name: string; description: string; - inputSchema: any; + inputSchema: JSONSchema; modelName: string; operation: OneUptimeOperation; modelType: ModelType; @@ -25,12 +51,18 @@ export interface ModelToolsResult { }; } +// Sort direction type +export type SortDirection = 1 | -1; + +// Sort object type +export type SortObject = Record; + export interface OneUptimeToolCallArgs { id?: string; - data?: any; - query?: any; - select?: any; + data?: JSONObject; + query?: JSONObject; + select?: JSONObject; skip?: number; limit?: number; - sort?: any; + sort?: SortObject; } diff --git a/MCP/Utils/DynamicToolGenerator.ts b/MCP/Utils/DynamicToolGenerator.ts index 980d73f87e..786c68b541 100644 --- a/MCP/Utils/DynamicToolGenerator.ts +++ b/MCP/Utils/DynamicToolGenerator.ts @@ -4,7 +4,11 @@ import DatabaseBaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/Da import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel"; import OneUptimeOperation from "../Types/OneUptimeOperation"; import ModelType from "../Types/ModelType"; -import { McpToolInfo, ModelToolsResult } from "../Types/McpTypes"; +import { + McpToolInfo, + ModelToolsResult, + JSONSchemaProperty, +} from "../Types/McpTypes"; import { ModelSchema, ModelSchemaType, @@ -15,6 +19,46 @@ import { } from "Common/Utils/Schema/AnalyticsModelSchema"; import MCPLogger from "./MCPLogger"; +// Type for Zod field definition +interface ZodFieldDef { + typeName?: string; + innerType?: ZodField; + description?: string; + openapi?: { + metadata?: OpenApiMetadata; + }; +} + +// Type for Zod field +interface ZodField { + _def?: ZodFieldDef; +} + +// Type for OpenAPI metadata +interface OpenApiMetadata { + type?: string; + description?: string; + example?: unknown; + format?: string; + default?: unknown; + items?: JSONSchemaProperty; +} + +// Type for Zod schema with shape +interface ZodSchemaWithShape { + _def?: { + shape?: () => Record; + }; +} + +// Type for zodToJsonSchema return value +interface ZodToJsonSchemaResult { + type: string; + properties: Record; + required?: string[]; + additionalProperties: boolean; +} + export default class DynamicToolGenerator { /** * Sanitize a name to be valid for MCP tool names @@ -41,15 +85,18 @@ export default class DynamicToolGenerator { */ private static zodToJsonSchema( zodSchema: ModelSchemaType | AnalyticsModelSchemaType, - ): any { + ): ZodToJsonSchemaResult { try { /* * The Zod schemas in this project are extended with OpenAPI metadata * We can extract the shape and create a basic JSON schema */ - const shape: any = (zodSchema as any)._def?.shape; + const schemaWithShape: ZodSchemaWithShape = + zodSchema as unknown as ZodSchemaWithShape; + const shapeFunction: (() => Record) | undefined = + schemaWithShape._def?.shape; - if (!shape) { + if (!shapeFunction) { return { type: "object", properties: {}, @@ -57,23 +104,24 @@ export default class DynamicToolGenerator { }; } - const properties: any = {}; + const shape: Record = shapeFunction(); + const properties: Record = {}; const required: string[] = []; - for (const [key, value] of Object.entries(shape())) { - const zodField: any = value as any; + for (const [key, value] of Object.entries(shape)) { + const zodField: ZodField = value; // Handle ZodOptional fields by looking at the inner type - let actualField: any = zodField; + let actualField: ZodField = zodField; let isOptional: boolean = false; if (zodField._def?.typeName === "ZodOptional") { - actualField = zodField._def.innerType; + actualField = zodField._def.innerType || zodField; isOptional = true; } // Extract OpenAPI metadata - it's stored in _def.openapi.metadata - const openApiMetadata: any = + const openApiMetadata: OpenApiMetadata | undefined = actualField._def?.openapi?.metadata || zodField._def?.openapi?.metadata; @@ -85,7 +133,7 @@ export default class DynamicToolGenerator { const cleanDescription: string = this.cleanDescription(rawDescription); if (openApiMetadata) { - const fieldSchema: any = { + const fieldSchema: JSONSchemaProperty = { type: openApiMetadata.type || "string", description: cleanDescription, ...(openApiMetadata.example !== undefined && { @@ -120,12 +168,17 @@ export default class DynamicToolGenerator { } } - return { + const result: ZodToJsonSchemaResult = { type: "object", properties, - required: required.length > 0 ? required : undefined, additionalProperties: false, }; + + if (required.length > 0) { + result.required = required; + } + + return result; } catch { return { type: "object", @@ -217,7 +270,8 @@ export default class DynamicToolGenerator { }); // CREATE Tool - const createSchemaProperties: any = this.zodToJsonSchema(createSchema); + const createSchemaProperties: ZodToJsonSchemaResult = + this.zodToJsonSchema(createSchema); tools.push({ name: `create_${this.sanitizeToolName(singularName)}`, description: `Create a new ${singularName} in OneUptime`, @@ -292,7 +346,8 @@ export default class DynamicToolGenerator { }); // UPDATE Tool - const updateSchemaProperties: any = this.zodToJsonSchema(updateSchema); + const updateSchemaProperties: ZodToJsonSchemaResult = + this.zodToJsonSchema(updateSchema); tools.push({ name: `update_${this.sanitizeToolName(singularName)}`, description: `Update an existing ${singularName} in OneUptime`, @@ -420,7 +475,7 @@ export default class DynamicToolGenerator { }); // CREATE Tool for Analytics - const analyticsCreateSchemaProperties: any = + const analyticsCreateSchemaProperties: ZodToJsonSchemaResult = this.zodToJsonSchema(createSchema); tools.push({ name: `create_${this.sanitizeToolName(singularName)}`,