mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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"];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
94
MCP/Index.ts
94
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(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum OneUptimeOperation {
|
||||
Create = "create",
|
||||
Read = "read",
|
||||
Read = "read",
|
||||
List = "list",
|
||||
Update = "update",
|
||||
Delete = "delete",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user