Refactor DynamicToolGenerator tests and utility functions for improved readability and consistency

- Enhanced test cases in DynamicToolGenerator.test.ts for better logging and structure.
- Updated OneUptimeOperation.ts to maintain consistent formatting.
- Refactored DynamicToolGenerator.ts for improved code clarity and organization, including consistent use of commas and spacing.
- Improved sanitization and JSON schema generation methods for better handling of OpenAPI metadata.
- Cleaned up description handling in DynamicToolGenerator to ensure proper formatting.
- Adjusted server.test.ts for consistent quotation marks and improved readability.
This commit is contained in:
Nawaz Dhandala
2025-06-30 23:27:57 +01:00
parent 3116100f1a
commit 122b0d6be7
12 changed files with 555 additions and 320 deletions

View File

@@ -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"];

View File

@@ -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,

View File

@@ -403,8 +403,6 @@ export default class BaseAPI<
await this.onBeforeCreate(req, res);
const body: JSONObject = req.body;
const item: TBaseModel = BaseModel.fromJSON<TBaseModel>(
body["data"] as JSONObject,
this.entityType,

View File

@@ -54,7 +54,7 @@ export default class QueryUtil {
query[key] = QueryHelper.equalToOrNull(
query[key] as any,
) as FindOperator<any> as any;
}else if (
} else if (
query[key] &&
query[key] instanceof EqualTo &&
tableColumnMetadata

View File

@@ -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);

View File

@@ -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 });
}

View File

@@ -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(", ")}`);
}
}

View File

@@ -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<any> {
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<any> | 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;

View File

@@ -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);
}
});

View File

@@ -1,6 +1,6 @@
export enum OneUptimeOperation {
Create = "create",
Read = "read",
Read = "read",
List = "list",
Update = "update",
Delete = "delete",

View File

@@ -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;
}
}

View File

@@ -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");
});
});