Refactor and clean up code formatting across multiple files

- Improved code formatting in AnnouncementsTable.tsx for better readability.
- Simplified description formatting in AnnouncementView.tsx.
- Adjusted lazy loading syntax in StatusPagesRoutes.tsx for consistency.
- Enhanced script formatting in MCPServerGenerator.ts for better clarity.
- Streamlined error handling and response parsing in OpenAPIParser.ts.
- Refined resource generation logic in ResourceGenerator.ts for improved maintainability.
- Updated comments and structure in GenerateMCPServer.ts for better understanding.
- General code cleanup and formatting adjustments across various files to adhere to style guidelines.
This commit is contained in:
Nawaz Dhandala
2025-06-26 13:51:11 +01:00
parent 8718e58dcb
commit cf6ee298cc
27 changed files with 820 additions and 579 deletions

View File

@@ -576,7 +576,7 @@ export default class Alert extends BaseModel {
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})

View File

@@ -348,9 +348,7 @@ export default class ApiKey extends BaseModel {
public expiresAt?: Date = undefined;
@ColumnAccessControl({
create: [
],
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,

View File

@@ -425,8 +425,8 @@ export default class APIKeyPermission extends BaseModel {
Permission.EditProjectApiKey,
],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})

View File

@@ -505,7 +505,7 @@ export default class Incident extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
@@ -547,7 +547,7 @@ export default class Incident extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
@@ -1100,9 +1100,7 @@ export default class Incident extends BaseModel {
public telemetryQuery?: TelemetryQuery = undefined;
@ColumnAccessControl({
create: [
],
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,

View File

@@ -473,10 +473,10 @@ export default class Monitor extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectIncident,
],
read: [
Permission.ProjectOwner,
@@ -495,7 +495,7 @@ export default class Monitor extends BaseModel {
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
isDefaultValueColumn: true,
isDefaultValueColumn: true,
title: "Current Monitor Status ID",
description: "Whats the current status ID of this monitor?",
canReadOnRelationQuery: true,

View File

@@ -828,8 +828,7 @@ export default class Project extends TenantModel {
hideColumnInDocumentation: true,
type: TableColumnType.Boolean,
title: "Not Enabled SMS or Call Notification Sent to Owners",
description:
"Not Enabled SMS or Call Notification Sent to Owners",
description: "Not Enabled SMS or Call Notification Sent to Owners",
defaultValue: false,
})
@Column({

View File

@@ -512,8 +512,8 @@ export default class ProjectSmtpConfig extends BaseModel {
Permission.EditProjectSMTPConfig,
],
})
@TableColumn({
required: true,
@TableColumn({
required: true,
type: TableColumnType.Boolean,
defaultValue: true,
})

View File

@@ -550,8 +550,8 @@ export default class ProjectSSO extends BaseModel {
Permission.EditProjectSSO,
],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})

View File

@@ -344,7 +344,11 @@ export default class PromoCode extends BaseModel {
read: [],
update: [],
})
@TableColumn({ isDefaultValueColumn: true, type: TableColumnType.Boolean, defaultValue: false })
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,

View File

@@ -451,8 +451,8 @@ export default class StatusPageAnnouncement extends BaseModel {
],
update: [],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
computed: true,
hideColumnInDocumentation: true,
type: TableColumnType.Boolean,

View File

@@ -552,8 +552,8 @@ export default class StatusPageSSO extends BaseModel {
Permission.EditStatusPageSSO,
],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@@ -578,8 +578,8 @@ export default class StatusPageSSO extends BaseModel {
],
update: [],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})

View File

@@ -431,8 +431,8 @@ export default class TeamPermission extends BaseModel {
Permission.EditProjectTeam,
],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})

View File

@@ -151,7 +151,11 @@ class User extends UserModel {
update: [],
})
@TableColumn({ isDefaultValueColumn: true, type: TableColumnType.Boolean, defaultValue: false })
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
@@ -539,7 +543,7 @@ class User extends UserModel {
update: [],
})
@TableColumn({
@TableColumn({
type: TableColumnType.OTP,
computed: true,
})

View File

@@ -245,8 +245,8 @@ class UserNotificationSetting extends BaseModel {
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})
@@ -261,8 +261,8 @@ class UserNotificationSetting extends BaseModel {
read: [Permission.CurrentUser],
update: [Permission.CurrentUser],
})
@TableColumn({
isDefaultValueColumn: true,
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
defaultValue: false,
})

View File

@@ -517,13 +517,12 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
(data as any)[columnName] = null;
}
// if table columntype is file and file is base64 stirng then convert to buffer to save.
// if table columntype is file and file is base64 stirng then convert to buffer to save.
if (
tableColumnMetadata.type === TableColumnType.File &&
(data as any)[columnName] &&
typeof (data as any)[columnName] === Typeof.String
) {
console.log("Here!");
const fileBuffer: Buffer = Buffer.from(

View File

@@ -155,7 +155,6 @@ export default class ModelPermission {
continue; // this is a special case where we want to force the default value on create.
}
throw new BadDataException(
`User is not allowed to ${requestType} on ${key} column of ${model.singularName}`,
);

View File

@@ -43,7 +43,7 @@ export default class ColumnPermissions {
public static getModelColumnsByPermissions<TBaseModel extends BaseModel>(
modelType: { new (): TBaseModel },
userPermissions: Array<UserPermission>,
requestType: DatabaseRequestType
requestType: DatabaseRequestType,
): Columns {
const model: BaseModel = new modelType();
const accessControl: Dictionary<ColumnAccessControl> =
@@ -54,7 +54,7 @@ export default class ColumnPermissions {
const permissions: Array<Permission> = userPermissions.map(
(item: UserPermission) => {
return item.permission;
}
},
);
for (const key in accessControl) {
@@ -80,7 +80,7 @@ export default class ColumnPermissions {
columnPermissions &&
PermissionHelper.doesPermissionsIntersect(
permissions,
columnPermissions
columnPermissions,
)
) {
columns.push(key);
@@ -95,19 +95,19 @@ export default class ColumnPermissions {
modelType: { new (): TBaseModel },
data: TBaseModel,
props: DatabaseCommonInteractionProps,
requestType: DatabaseRequestType
requestType: DatabaseRequestType,
): void {
const model: BaseModel = new modelType();
const userPermissions: Array<UserPermission> =
DatabaseCommonInteractionPropsUtil.getUserPermissions(
props,
PermissionType.Allow
PermissionType.Allow,
);
const permissionColumns: Columns = this.getModelColumnsByPermissions(
modelType,
userPermissions,
requestType
requestType,
);
const excludedColumnNames: Array<string> = this.getExcludedColumnNames();
@@ -132,7 +132,7 @@ export default class ColumnPermissions {
if (!tableColumnMetadata) {
throw new BadDataException(
`No TableColumnMetadata found for ${key} column of ${model.singularName}`
`No TableColumnMetadata found for ${key} column of ${model.singularName}`,
);
}
@@ -159,7 +159,7 @@ export default class ColumnPermissions {
}
throw new BadDataException(
`User is not allowed to ${requestType} on ${key} column of ${model.singularName}`
`User is not allowed to ${requestType} on ${key} column of ${model.singularName}`,
);
}
@@ -179,13 +179,13 @@ export default class ColumnPermissions {
!SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(
billingAccessControl.create,
props.currentPlan,
getAllEnvVars()
getAllEnvVars(),
)
) {
throw new PaymentRequiredException(
"Please upgrade your plan to " +
billingAccessControl.create +
" to access this feature"
" to access this feature",
);
}
}
@@ -198,13 +198,13 @@ export default class ColumnPermissions {
!SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(
billingAccessControl.read,
props.currentPlan,
getAllEnvVars()
getAllEnvVars(),
)
) {
throw new PaymentRequiredException(
"Please upgrade your plan to " +
billingAccessControl.read +
" to access this feature"
" to access this feature",
);
}
}
@@ -217,13 +217,13 @@ export default class ColumnPermissions {
!SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(
billingAccessControl.update,
props.currentPlan,
getAllEnvVars()
getAllEnvVars(),
)
) {
throw new PaymentRequiredException(
"Please upgrade your plan to " +
billingAccessControl.update +
" to access this feature"
" to access this feature",
);
}
}

View File

@@ -114,13 +114,13 @@ export default class Text {
}
public static isBase64(text: string): boolean {
if (!text || typeof text !== 'string') {
if (!text || typeof text !== "string") {
return false;
}
// Remove data URI prefix if present (e.g., data:image/jpeg;base64,)
const base64String = text.replace(/^data:[^;]+;base64,/, '');
const base64String = text.replace(/^data:[^;]+;base64,/, "");
// Check if string is empty after removing prefix
if (!base64String) {
return false;
@@ -137,13 +137,13 @@ export default class Text {
}
public static extractBase64FromDataUri(text: string): string {
if (!text || typeof text !== 'string') {
if (!text || typeof text !== "string") {
return text;
}
// Check if it's a data URI
if (text.startsWith('data:')) {
const base64Index = text.indexOf(';base64,');
if (text.startsWith("data:")) {
const base64Index = text.indexOf(";base64,");
if (base64Index !== -1) {
return text.substring(base64Index + 8); // 8 is length of ';base64,'
}
@@ -154,13 +154,13 @@ export default class Text {
}
public static extractMimeTypeFromDataUri(text: string): string | null {
if (!text || typeof text !== 'string') {
if (!text || typeof text !== "string") {
return null;
}
// Check if it's a data URI
if (text.startsWith('data:')) {
const mimeTypeEnd = text.indexOf(';');
if (text.startsWith("data:")) {
const mimeTypeEnd = text.indexOf(";");
if (mimeTypeEnd !== -1) {
return text.substring(5, mimeTypeEnd); // 5 is length of 'data:'
}

View File

@@ -50,74 +50,135 @@ export class AnalyticsModelSchema extends BaseSchema {
let zodType: ZodTypes.ZodTypeAny;
if (column.type === TableColumnType.ObjectID) {
zodType = z.string().openapi(this.addDefaultToOpenApi({
type: "string",
example: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
}, column));
zodType = z.string().openapi(
this.addDefaultToOpenApi(
{
type: "string",
example: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
},
column,
),
);
} else if (column.type === TableColumnType.Date) {
zodType = z.date().openapi(this.addDefaultToOpenApi({
type: "string",
format: "date-time",
example: "2023-01-15T12:30:00.000Z",
}, column));
zodType = z.date().openapi(
this.addDefaultToOpenApi(
{
type: "string",
format: "date-time",
example: "2023-01-15T12:30:00.000Z",
},
column,
),
);
} else if (column.type === TableColumnType.Text) {
zodType = z.string().openapi(this.addDefaultToOpenApi({
type: "string",
example: "Example text value",
}, column));
zodType = z.string().openapi(
this.addDefaultToOpenApi(
{
type: "string",
example: "Example text value",
},
column,
),
);
} else if (column.type === TableColumnType.Number) {
zodType = z.number().openapi(this.addDefaultToOpenApi({ type: "number", example: 42 }, column));
zodType = z
.number()
.openapi(
this.addDefaultToOpenApi({ type: "number", example: 42 }, column),
);
} else if (column.type === TableColumnType.LongNumber) {
zodType = z.number().openapi(this.addDefaultToOpenApi({
type: "number",
example: 1000000,
}, column));
zodType = z.number().openapi(
this.addDefaultToOpenApi(
{
type: "number",
example: 1000000,
},
column,
),
);
} else if (column.type === TableColumnType.Boolean) {
zodType = z.boolean().openapi(this.addDefaultToOpenApi({ type: "boolean", example: true }, column));
zodType = z
.boolean()
.openapi(
this.addDefaultToOpenApi(
{ type: "boolean", example: true },
column,
),
);
} else if (column.type === TableColumnType.JSON) {
zodType = z.any().openapi(this.addDefaultToOpenApi({
type: "object",
example: { key: "value", nested: { data: 123 } },
}, column));
zodType = z.any().openapi(
this.addDefaultToOpenApi(
{
type: "object",
example: { key: "value", nested: { data: 123 } },
},
column,
),
);
} else if (column.type === TableColumnType.JSONArray) {
zodType = z.array(z.any()).openapi(this.addDefaultToOpenApi({
type: "array",
items: {
type: "object",
},
example: [{ key: "value" }, { key2: "value2" }],
}, column));
zodType = z.array(z.any()).openapi(
this.addDefaultToOpenApi(
{
type: "array",
items: {
type: "object",
},
example: [{ key: "value" }, { key2: "value2" }],
},
column,
),
);
} else if (column.type === TableColumnType.Decimal) {
zodType = z.number().openapi(this.addDefaultToOpenApi({
type: "number",
example: 123.45,
}, column));
zodType = z.number().openapi(
this.addDefaultToOpenApi(
{
type: "number",
example: 123.45,
},
column,
),
);
} else if (column.type === TableColumnType.ArrayNumber) {
zodType = z.array(z.number()).openapi(this.addDefaultToOpenApi({
type: "array",
items: {
type: "number",
},
example: [1, 2, 3, 4, 5],
}, column));
zodType = z.array(z.number()).openapi(
this.addDefaultToOpenApi(
{
type: "array",
items: {
type: "number",
},
example: [1, 2, 3, 4, 5],
},
column,
),
);
} else if (column.type === TableColumnType.ArrayText) {
zodType = z.array(z.string()).openapi(this.addDefaultToOpenApi({
type: "array",
items: {
type: "string",
},
example: ["item1", "item2", "item3"],
}, column));
zodType = z.array(z.string()).openapi(
this.addDefaultToOpenApi(
{
type: "array",
items: {
type: "string",
},
example: ["item1", "item2", "item3"],
},
column,
),
);
} else if (column.type === TableColumnType.IP) {
zodType = IP.getSchema();
} else if (column.type === TableColumnType.Port) {
zodType = Port.getSchema();
} else {
// Default fallback
zodType = z.any().openapi(this.addDefaultToOpenApi({
type: "string",
example: "example_value",
}, column));
zodType = z.any().openapi(
this.addDefaultToOpenApi(
{
type: "string",
example: "example_value",
},
column,
),
);
}
// Apply default value if it exists
@@ -151,56 +212,107 @@ export class AnalyticsModelSchema extends BaseSchema {
case TableColumnType.Date:
return OneUptimeDate.getSchema();
case TableColumnType.Text:
return z.string().openapi(this.addDefaultToOpenApi({
type: "string",
example: "Example text",
}, column));
return z.string().openapi(
this.addDefaultToOpenApi(
{
type: "string",
example: "Example text",
},
column,
),
);
case TableColumnType.Number:
return z.number().openapi(this.addDefaultToOpenApi({ type: "number", example: 42 }, column));
return z
.number()
.openapi(
this.addDefaultToOpenApi({ type: "number", example: 42 }, column),
);
case TableColumnType.LongNumber:
return z.number().openapi(this.addDefaultToOpenApi({
type: "number",
example: 1000000,
}, column));
return z.number().openapi(
this.addDefaultToOpenApi(
{
type: "number",
example: 1000000,
},
column,
),
);
case TableColumnType.Boolean:
return z.boolean().openapi(this.addDefaultToOpenApi({ type: "boolean", example: true }, column));
return z
.boolean()
.openapi(
this.addDefaultToOpenApi(
{ type: "boolean", example: true },
column,
),
);
case TableColumnType.JSON:
return z.any().openapi(this.addDefaultToOpenApi({
type: "object",
example: { key: "value" },
}, column));
return z.any().openapi(
this.addDefaultToOpenApi(
{
type: "object",
example: { key: "value" },
},
column,
),
);
case TableColumnType.JSONArray:
return z.array(z.any()).openapi(this.addDefaultToOpenApi({
type: "array",
items: { type: "object" },
example: [{ key: "value" }],
}, column));
return z.array(z.any()).openapi(
this.addDefaultToOpenApi(
{
type: "array",
items: { type: "object" },
example: [{ key: "value" }],
},
column,
),
);
case TableColumnType.Decimal:
return z.number().openapi(this.addDefaultToOpenApi({
type: "number",
example: 123.45,
}, column));
return z.number().openapi(
this.addDefaultToOpenApi(
{
type: "number",
example: 123.45,
},
column,
),
);
case TableColumnType.ArrayNumber:
return z.array(z.number()).openapi(this.addDefaultToOpenApi({
type: "array",
items: { type: "number" },
example: [1, 2, 3],
}, column));
return z.array(z.number()).openapi(
this.addDefaultToOpenApi(
{
type: "array",
items: { type: "number" },
example: [1, 2, 3],
},
column,
),
);
case TableColumnType.ArrayText:
return z.array(z.string()).openapi(this.addDefaultToOpenApi({
type: "array",
items: { type: "string" },
example: ["item1", "item2"],
}, column));
return z.array(z.string()).openapi(
this.addDefaultToOpenApi(
{
type: "array",
items: { type: "string" },
example: ["item1", "item2"],
},
column,
),
);
case TableColumnType.IP:
return IP.getSchema();
case TableColumnType.Port:
return Port.getSchema();
default:
return z.any().openapi(this.addDefaultToOpenApi({
type: "string",
example: "example_value",
}, column));
return z.any().openapi(
this.addDefaultToOpenApi(
{
type: "string",
example: "example_value",
},
column,
),
);
}
}

View File

@@ -145,13 +145,14 @@ export class ModelSchema extends BaseSchema {
.string()
.openapi({ type: "string", example: "example-slug-value" });
} else if (column.type === TableColumnType.ShortText) {
const openapiConfig: any = { type: "string", example: "Example short text" };
const openapiConfig: any = {
type: "string",
example: "Example short text",
};
if (column.defaultValue !== undefined) {
openapiConfig.default = column.defaultValue;
}
zodType = z
.string()
.openapi(openapiConfig);
zodType = z.string().openapi(openapiConfig);
} else if (column.type === TableColumnType.LongText) {
const openapiConfig: any = {
type: "string",
@@ -383,7 +384,7 @@ export class ModelSchema extends BaseSchema {
// add title and description to the schema
let finalDescription = "";
// Add column description first if it exists
if (column.description) {
finalDescription = column.description;
@@ -1152,10 +1153,13 @@ export class ModelSchema extends BaseSchema {
// Check if the column is required and make it optional if not
// Also make columns with default values optional in create schemas
if(column.isDefaultValueColumn){
if (column.isDefaultValueColumn) {
// should be optional
zodType = zodType.optional();
} else if (column.title?.toLowerCase() === "project id" && column.type === TableColumnType.ObjectID) {
} else if (
column.title?.toLowerCase() === "project id" &&
column.type === TableColumnType.ObjectID
) {
// this is optional in the API as well as it's derived from API key
zodType = zodType.optional();
} else if (column.required) {
@@ -1171,7 +1175,7 @@ export class ModelSchema extends BaseSchema {
// Add title and description to the schema
let finalDescription = "";
// Add column description first if it exists
if (column.description) {
finalDescription = column.description;
@@ -1235,164 +1239,241 @@ export class ModelSchema extends BaseSchema {
} else if (column.type === TableColumnType.Date) {
zodType = OneUptimeDate.getSchema();
} else if (column.type === TableColumnType.VeryLongText) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example:
"This is an example of very long text content that might be stored in this field. It can contain a lot of information, such as detailed descriptions, comments, or any other lengthy text data that needs to be stored in the database.",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example:
"This is an example of very long text content that might be stored in this field. It can contain a lot of information, such as detailed descriptions, comments, or any other lengthy text data that needs to be stored in the database.",
}),
);
} else if (
column.type === TableColumnType.Number ||
column.type === TableColumnType.PositiveNumber
) {
zodType = z.number().openapi(addDefaultToOpenApi({ type: "number", example: 42 }));
zodType = z
.number()
.openapi(addDefaultToOpenApi({ type: "number", example: 42 }));
} else if (column.type === TableColumnType.Email) {
zodType = Email.getSchema();
} else if (column.type === TableColumnType.HashedString) {
zodType = z
.string()
.openapi(addDefaultToOpenApi({ type: "string", example: "hashed_string_value" }));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "hashed_string_value",
}),
);
} else if (column.type === TableColumnType.Slug) {
zodType = z
.string()
.openapi(addDefaultToOpenApi({ type: "string", example: "example-slug-value" }));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "example-slug-value",
}),
);
} else if (column.type === TableColumnType.ShortText) {
zodType = z
.string()
.openapi(addDefaultToOpenApi({ type: "string", example: "Example short text" }));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "Example short text",
}),
);
} else if (column.type === TableColumnType.LongText) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example:
"This is an example of longer text content that might be stored in this field.",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example:
"This is an example of longer text content that might be stored in this field.",
}),
);
} else if (column.type === TableColumnType.Phone) {
zodType = Phone.getSchema();
} else if (column.type === TableColumnType.Version) {
zodType = Version.getSchema();
} else if (column.type === TableColumnType.Password) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
format: "password",
example: "••••••••",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
format: "password",
example: "••••••••",
}),
);
} else if (column.type === TableColumnType.Name) {
zodType = Name.getSchema();
} else if (column.type === TableColumnType.Description) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "This is a description of the item",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "This is a description of the item",
}),
);
} else if (column.type === TableColumnType.File) {
zodType = z.any().openapi(addDefaultToOpenApi({
type: "string",
format: "binary",
}));
zodType = z.any().openapi(
addDefaultToOpenApi({
type: "string",
format: "binary",
}),
);
} else if (column.type === TableColumnType.Buffer) {
zodType = z.any().openapi(addDefaultToOpenApi({
type: "string",
format: "binary",
}));
zodType = z.any().openapi(
addDefaultToOpenApi({
type: "string",
format: "binary",
}),
);
} else if (column.type === TableColumnType.ShortURL) {
zodType = z.string().url().openapi(addDefaultToOpenApi({
type: "string",
example: "https://short.url/abc123",
}));
zodType = z
.string()
.url()
.openapi(
addDefaultToOpenApi({
type: "string",
example: "https://short.url/abc123",
}),
);
} else if (column.type === TableColumnType.Markdown) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "# Heading\n\nThis is **markdown** content",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "# Heading\n\nThis is **markdown** content",
}),
);
} else if (column.type === TableColumnType.Domain) {
zodType = Domain.getSchema();
} else if (column.type === TableColumnType.LongURL) {
zodType = z.string().url().openapi(addDefaultToOpenApi({
type: "string",
example: "https://www.example.com/path/to/resource?param=value",
}));
zodType = z
.string()
.url()
.openapi(
addDefaultToOpenApi({
type: "string",
example: "https://www.example.com/path/to/resource?param=value",
}),
);
} else if (column.type === TableColumnType.OTP) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "123456",
}));
} else if (column.type === TableColumnType.HTML) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "<div><h1>Title</h1><p>Content</p></div>",
}));
} else if (column.type === TableColumnType.JavaScript) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "function example() { return true; }",
}));
} else if (column.type === TableColumnType.CSS) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "body { color: #333; margin: 0; }",
}));
} else if (column.type === TableColumnType.Array) {
zodType = z.array(z.any()).openapi(addDefaultToOpenApi({
type: "array",
items: {
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
},
example: ["item1", "item2", "item3"],
}));
example: "123456",
}),
);
} else if (column.type === TableColumnType.HTML) {
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "<div><h1>Title</h1><p>Content</p></div>",
}),
);
} else if (column.type === TableColumnType.JavaScript) {
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "function example() { return true; }",
}),
);
} else if (column.type === TableColumnType.CSS) {
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "body { color: #333; margin: 0; }",
}),
);
} else if (column.type === TableColumnType.Array) {
zodType = z.array(z.any()).openapi(
addDefaultToOpenApi({
type: "array",
items: {
type: "string",
},
example: ["item1", "item2", "item3"],
}),
);
} else if (column.type === TableColumnType.SmallPositiveNumber) {
zodType = z.number().int().nonnegative().openapi(addDefaultToOpenApi({
type: "integer",
example: 5,
}));
zodType = z
.number()
.int()
.nonnegative()
.openapi(
addDefaultToOpenApi({
type: "integer",
example: 5,
}),
);
} else if (column.type === TableColumnType.BigPositiveNumber) {
zodType = z.number().nonnegative().openapi(addDefaultToOpenApi({
type: "number",
example: 1000000,
}));
zodType = z
.number()
.nonnegative()
.openapi(
addDefaultToOpenApi({
type: "number",
example: 1000000,
}),
);
} else if (column.type === TableColumnType.SmallNumber) {
zodType = z.number().int().openapi(addDefaultToOpenApi({
type: "integer",
example: 10,
}));
zodType = z
.number()
.int()
.openapi(
addDefaultToOpenApi({
type: "integer",
example: 10,
}),
);
} else if (column.type === TableColumnType.BigNumber) {
zodType = z.number().openapi(addDefaultToOpenApi({
type: "number",
example: 1000000,
}));
zodType = z.number().openapi(
addDefaultToOpenApi({
type: "number",
example: 1000000,
}),
);
} else if (column.type === TableColumnType.Permission) {
zodType = z.any().openapi(addDefaultToOpenApi({
type: "object",
example: { read: true, write: false, delete: false },
}));
zodType = z.any().openapi(
addDefaultToOpenApi({
type: "object",
example: { read: true, write: false, delete: false },
}),
);
} else if (column.type === TableColumnType.CustomFieldType) {
zodType = z.any().openapi(addDefaultToOpenApi({
type: "object",
example: { type: "text", required: true },
}));
zodType = z.any().openapi(
addDefaultToOpenApi({
type: "object",
example: { type: "text", required: true },
}),
);
} else if (column.type === TableColumnType.MonitorType) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "HTTP",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "HTTP",
}),
);
} else if (column.type === TableColumnType.WorkflowStatus) {
zodType = z.string().openapi(addDefaultToOpenApi({
type: "string",
example: "In Progress",
}));
zodType = z.string().openapi(
addDefaultToOpenApi({
type: "string",
example: "In Progress",
}),
);
} else if (column.type === TableColumnType.Boolean) {
zodType = z.boolean().openapi(addDefaultToOpenApi({ type: "boolean", example: true }));
zodType = z
.boolean()
.openapi(addDefaultToOpenApi({ type: "boolean", example: true }));
} else if (column.type === TableColumnType.JSON) {
zodType = z.any().openapi(addDefaultToOpenApi({
type: "object",
example: { key: "value", nested: { data: 123 } },
}));
zodType = z.any().openapi(
addDefaultToOpenApi({
type: "object",
example: { key: "value", nested: { data: 123 } },
}),
);
} else if (column.type === TableColumnType.EntityArray) {
const entityArrayType: (new () => DatabaseBaseModel) | undefined =
column.modelType;
if (!entityArrayType) {
return z.any().openapi(addDefaultToOpenApi({
type: "array",
items: { type: "object" },
example: [],
}));
return z.any().openapi(
addDefaultToOpenApi({
type: "array",
items: { type: "object" },
example: [],
}),
);
}
// Use the appropriate schema method based on the operation type
@@ -1420,26 +1501,30 @@ export class ModelSchema extends BaseSchema {
});
}),
)
.openapi(addDefaultToOpenApi({
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
.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;
if (!entityType) {
return z.any().openapi(addDefaultToOpenApi({
type: "object",
description: "Entity reference",
example: { _id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
}));
return z.any().openapi(
addDefaultToOpenApi({
type: "object",
description: "Entity reference",
example: { _id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
}),
);
}
// Use the appropriate schema method based on the operation type
@@ -1463,12 +1548,16 @@ export class ModelSchema extends BaseSchema {
.lazy(() => {
return schema;
})
.openapi(addDefaultToOpenApi({
type: "object",
example: { id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
}));
.openapi(
addDefaultToOpenApi({
type: "object",
example: { id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
}),
);
} else {
zodType = z.any().openapi(addDefaultToOpenApi({ type: "null", example: null }));
zodType = z
.any()
.openapi(addDefaultToOpenApi({ type: "null", example: null }));
}
// Apply default value if it exists in the column metadata

View File

@@ -132,7 +132,9 @@ const AnnouncementTable: FunctionComponent<ComponentProps> = (
noItemsMessage={"No announcements found."}
createEditModalWidth={ModalWidth.Large}
showRefreshButton={true}
viewPageRoute={RouteUtil.populateRouteParams(RouteMap[PageMap.STATUS_PAGE_ANNOUNCEMENTS] as Route)}
viewPageRoute={RouteUtil.populateRouteParams(
RouteMap[PageMap.STATUS_PAGE_ANNOUNCEMENTS] as Route,
)}
filters={[
{
field: {

View File

@@ -101,8 +101,7 @@ const AnnouncementView: FunctionComponent<
},
title: "Show announcement on these status pages",
stepId: "status-pages",
description:
"Select status pages to show this announcement on",
description: "Select status pages to show this announcement on",
fieldType: FormFieldSchemaType.MultiSelectDropdown,
dropdownModal: {
type: StatusPage,
@@ -237,9 +236,7 @@ const AnnouncementView: FunctionComponent<
modelId: modelId,
}}
/>
<div className="mt-4">
</div>
<div className="mt-4"></div>
<ModelDelete
modelType={StatusPageAnnouncement}

View File

@@ -165,11 +165,10 @@ const AnnouncementCreate: LazyExoticComponent<
return import("../Pages/StatusPages/AnnouncementCreate");
});
const AnnouncementView: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/StatusPages/AnnouncementView");
});
const AnnouncementView: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/StatusPages/AnnouncementView");
});
const StatusPagesRoutes: FunctionComponent<ComponentProps> = (
props: ComponentProps,

View File

@@ -37,20 +37,23 @@ export class MCPServerGenerator {
},
files: ["build", "README.md", "package.json"],
scripts: {
start: "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts",
start:
"export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts",
build: "tsc",
compile: "tsc",
dev: "npx nodemon",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
"clear-modules":
"rm -rf node_modules && rm package-lock.json && npm install",
audit: "npm audit --audit-level=low",
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
"dep-check":
"npm install -g depcheck && depcheck ./ --skip-missing=true",
test: "rm -rf build && jest --detectOpenHandles --passWithNoTests",
coverage: "jest --detectOpenHandles --coverage",
prepublishOnly: "npm run build",
},
keywords: [
"mcp",
"model-context-protocol",
"model-context-protocol",
"oneuptime",
"api",
"monitoring",
@@ -90,60 +93,60 @@ export class MCPServerGenerator {
private async generateIndexFile(): Promise<void> {
const indexContent = [
'#!/usr/bin/env node',
'',
"#!/usr/bin/env node",
"",
'import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";',
'import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";',
'import { ServerConfig } from "./Utils/Config.js";',
'import { MCPService } from "./Service/MCP.js";',
'import dotenv from "dotenv";',
'',
'// Load environment variables',
'dotenv.config();',
'',
'async function main(): Promise<void> {',
' try {',
' // Create server instance',
' const server: McpServer = new McpServer({',
' name: ServerConfig.name,',
' version: ServerConfig.version,',
' capabilities: {',
' tools: {},',
' resources: {},',
' },',
' });',
'',
' // Add tools to server',
' const mcpService = new MCPService();',
' await mcpService.addToolsToServer(server);',
'',
' const transport: StdioServerTransport = new StdioServerTransport();',
' await server.connect(transport);',
"",
"// Load environment variables",
"dotenv.config();",
"",
"async function main(): Promise<void> {",
" try {",
" // Create server instance",
" const server: McpServer = new McpServer({",
" name: ServerConfig.name,",
" version: ServerConfig.version,",
" capabilities: {",
" tools: {},",
" resources: {},",
" },",
" });",
"",
" // Add tools to server",
" const mcpService = new MCPService();",
" await mcpService.addToolsToServer(server);",
"",
" const transport: StdioServerTransport = new StdioServerTransport();",
" await server.connect(transport);",
' console.error("OneUptime MCP Server running on stdio");',
' } catch (error) {',
" } catch (error) {",
' console.error("Fatal error in main():");',
' console.error(error);',
' process.exit(1);',
' }',
'}',
'',
'// Handle graceful shutdown',
" console.error(error);",
" process.exit(1);",
" }",
"}",
"",
"// Handle graceful shutdown",
'process.on("SIGINT", () => {',
' console.error("Received SIGINT, shutting down gracefully...");',
' process.exit(0);',
'});',
'',
" process.exit(0);",
"});",
"",
'process.on("SIGTERM", () => {',
' console.error("Received SIGTERM, shutting down gracefully...");',
' process.exit(0);',
'});',
'',
'main().catch((error: Error) => {',
" process.exit(0);",
"});",
"",
"main().catch((error: Error) => {",
' console.error("Fatal error in main():");',
' console.error(error);',
' process.exit(1);',
'});',
].join('\n');
" console.error(error);",
" process.exit(1);",
"});",
].join("\n");
await this.fileGenerator.writeFile("Index.ts", indexContent);
}
@@ -154,40 +157,45 @@ export class MCPServerGenerator {
const tools = parser.getMCPTools();
const toolRegistrations = tools
.map((tool) => [
` server.tool(`,
` "${tool.name}",`,
` "${StringUtils.sanitizeDescription(tool.description)}",`,
` ${JSON.stringify(tool.inputSchema, null, 6).replace(/^/gm, " ")},`,
` async (args: any) => {`,
` return await this.${StringUtils.toCamelCase(tool.name)}(args);`,
` }`,
` );`,
].join('\n')).join('\n\n');
.map((tool) => {
return [
` server.tool(`,
` "${tool.name}",`,
` "${StringUtils.sanitizeDescription(tool.description)}",`,
` ${JSON.stringify(tool.inputSchema, null, 6).replace(/^/gm, " ")},`,
` async (args: any) => {`,
` return await this.${StringUtils.toCamelCase(tool.name)}(args);`,
` }`,
` );`,
].join("\n");
})
.join("\n\n");
const toolMethods = tools
.map((tool) => this.generateToolMethod(tool))
.map((tool) => {
return this.generateToolMethod(tool);
})
.join("\n\n");
const serviceContent = [
'import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";',
'import { OneUptimeAPIClient } from "./APIClient.js";',
'',
'export class MCPService {',
' private apiClient: OneUptimeAPIClient;',
'',
' public constructor() {',
' this.apiClient = new OneUptimeAPIClient();',
' }',
'',
' public async addToolsToServer(server: McpServer): Promise<void> {',
' // Register all tools',
"",
"export class MCPService {",
" private apiClient: OneUptimeAPIClient;",
"",
" public constructor() {",
" this.apiClient = new OneUptimeAPIClient();",
" }",
"",
" public async addToolsToServer(server: McpServer): Promise<void> {",
" // Register all tools",
toolRegistrations,
' }',
'',
" }",
"",
toolMethods,
'}',
].join('\n');
"}",
].join("\n");
this.fileGenerator.ensureDirectoryExists("Service");
await this.fileGenerator.writeFile("Service/MCP.ts", serviceContent);
@@ -196,151 +204,151 @@ export class MCPServerGenerator {
private generateToolMethod(tool: MCPTool): string {
const methodName = StringUtils.toCamelCase(tool.name);
const operation = tool.operation;
return [
` private async ${methodName}(args: any): Promise<any> {`,
' try {',
' const response = await this.apiClient.request({',
" try {",
" const response = await this.apiClient.request({",
` method: "${operation.method.toUpperCase()}",`,
` path: "${operation.path}",`,
' data: args,',
' });',
'',
' return {',
' content: [',
' {',
" data: args,",
" });",
"",
" return {",
" content: [",
" {",
' type: "text",',
' text: JSON.stringify(response.data, null, 2),',
' },',
' ],',
' };',
' } catch (error) {',
" text: JSON.stringify(response.data, null, 2),",
" },",
" ],",
" };",
" } catch (error) {",
' throw new Error(`API request failed: ${error instanceof Error ? error.message : "Unknown error"}`);',
' }',
' }',
].join('\n');
" }",
" }",
].join("\n");
}
private async generateAPIClient(): Promise<void> {
const clientContent = [
'import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";',
'',
'export interface APIRequestConfig {',
' method: string;',
' path: string;',
' data?: any;',
' params?: any;',
' headers?: Record<string, string>;',
'}',
'',
'export class OneUptimeAPIClient {',
' private client: AxiosInstance;',
' private baseURL: string;',
' private apiKey: string;',
'',
' public constructor() {',
' this.baseURL = this.getBaseURL();',
' this.apiKey = this.getAPIKey();',
'',
' this.client = axios.create({',
' baseURL: this.baseURL,',
' timeout: 30000,',
' headers: {',
"",
"export interface APIRequestConfig {",
" method: string;",
" path: string;",
" data?: any;",
" params?: any;",
" headers?: Record<string, string>;",
"}",
"",
"export class OneUptimeAPIClient {",
" private client: AxiosInstance;",
" private baseURL: string;",
" private apiKey: string;",
"",
" public constructor() {",
" this.baseURL = this.getBaseURL();",
" this.apiKey = this.getAPIKey();",
"",
" this.client = axios.create({",
" baseURL: this.baseURL,",
" timeout: 30000,",
" headers: {",
' "Content-Type": "application/json",',
' "Accept": "application/json",',
' "User-Agent": "OneUptime MCP Server/1.0.0",',
' },',
' });',
'',
' // Add request interceptor for authentication',
' this.client.interceptors.request.use((config) => {',
' if (this.apiKey) {',
" },",
" });",
"",
" // Add request interceptor for authentication",
" this.client.interceptors.request.use((config) => {",
" if (this.apiKey) {",
' config.headers["APIKey"] = this.apiKey;',
' }',
' return config;',
' });',
'',
' // Add response interceptor for error handling',
' this.client.interceptors.response.use(',
' (response) => response,',
' (error) => {',
' if (error.response) {',
' const errorMessage = error.response.data?.message || error.response.statusText;',
' throw new Error(`HTTP ${error.response.status}: ${errorMessage}`);',
' } else if (error.request) {',
" }",
" return config;",
" });",
"",
" // Add response interceptor for error handling",
" this.client.interceptors.response.use(",
" (response) => response,",
" (error) => {",
" if (error.response) {",
" const errorMessage = error.response.data?.message || error.response.statusText;",
" throw new Error(`HTTP ${error.response.status}: ${errorMessage}`);",
" } else if (error.request) {",
' throw new Error("Network error: No response received from server");',
' } else {',
' throw new Error(`Request error: ${error.message}`);',
' }',
' }',
' );',
' }',
'',
' private getBaseURL(): string {',
" } else {",
" throw new Error(`Request error: ${error.message}`);",
" }",
" }",
" );",
" }",
"",
" private getBaseURL(): string {",
' const url = process.env.ONEUPTIME_URL || process.env.ONEUPTIME_API_URL || "https://oneuptime.com";',
' ',
' // Ensure the URL has the correct scheme',
" ",
" // Ensure the URL has the correct scheme",
' const normalizedURL = url.startsWith("http") ? url : `https://${url}`;',
' ',
' // Append /api if not present',
" ",
" // Append /api if not present",
' return normalizedURL.endsWith("/api") ? normalizedURL : `${normalizedURL.replace(/\\/$/, "")}/api`;',
' }',
'',
' private getAPIKey(): string {',
' const apiKey = process.env.ONEUPTIME_API_KEY || process.env.API_KEY;',
' if (!apiKey) {',
' throw new Error(',
" }",
"",
" private getAPIKey(): string {",
" const apiKey = process.env.ONEUPTIME_API_KEY || process.env.API_KEY;",
" if (!apiKey) {",
" throw new Error(",
' "OneUptime API key is required. Set ONEUPTIME_API_KEY or API_KEY environment variable."',
' );',
' }',
' return apiKey;',
' }',
'',
' public async request(config: APIRequestConfig): Promise<AxiosResponse> {',
' const requestConfig: AxiosRequestConfig = {',
' method: config.method.toLowerCase() as any,',
' url: this.interpolatePath(config.path, config.data || config.params),',
" );",
" }",
" return apiKey;",
" }",
"",
" public async request(config: APIRequestConfig): Promise<AxiosResponse> {",
" const requestConfig: AxiosRequestConfig = {",
" method: config.method.toLowerCase() as any,",
" url: this.interpolatePath(config.path, config.data || config.params),",
' data: config.method.toUpperCase() !== "GET" ? config.data : undefined,',
' params: config.method.toUpperCase() === "GET" ? config.params : undefined,',
' headers: config.headers || {},',
' };',
'',
' return await this.client.request(requestConfig);',
' }',
'',
' private interpolatePath(path: string, data: any): string {',
' if (!data) return path;',
'',
' return path.replace(/\\{([^}]+)\\}/g, (match, paramName) => {',
' const value = data[paramName];',
' if (value === undefined) {',
' throw new Error(`Missing required path parameter: ${paramName}`);',
' }',
' return encodeURIComponent(value.toString());',
' });',
' }',
'',
' public async get(path: string, params?: any): Promise<AxiosResponse> {',
" headers: config.headers || {},",
" };",
"",
" return await this.client.request(requestConfig);",
" }",
"",
" private interpolatePath(path: string, data: any): string {",
" if (!data) return path;",
"",
" return path.replace(/\\{([^}]+)\\}/g, (match, paramName) => {",
" const value = data[paramName];",
" if (value === undefined) {",
" throw new Error(`Missing required path parameter: ${paramName}`);",
" }",
" return encodeURIComponent(value.toString());",
" });",
" }",
"",
" public async get(path: string, params?: any): Promise<AxiosResponse> {",
' return this.request({ method: "GET", path, params });',
' }',
'',
' public async post(path: string, data?: any): Promise<AxiosResponse> {',
" }",
"",
" public async post(path: string, data?: any): Promise<AxiosResponse> {",
' return this.request({ method: "POST", path, data });',
' }',
'',
' public async put(path: string, data?: any): Promise<AxiosResponse> {',
" }",
"",
" public async put(path: string, data?: any): Promise<AxiosResponse> {",
' return this.request({ method: "PUT", path, data });',
' }',
'',
' public async patch(path: string, data?: any): Promise<AxiosResponse> {',
" }",
"",
" public async patch(path: string, data?: any): Promise<AxiosResponse> {",
' return this.request({ method: "PATCH", path, data });',
' }',
'',
' public async delete(path: string): Promise<AxiosResponse> {',
" }",
"",
" public async delete(path: string): Promise<AxiosResponse> {",
' return this.request({ method: "DELETE", path });',
' }',
'}',
].join('\n');
" }",
"}",
].join("\n");
this.fileGenerator.ensureDirectoryExists("Service");
await this.fileGenerator.writeFile("Service/APIClient.ts", clientContent);
@@ -348,41 +356,41 @@ export class MCPServerGenerator {
private async generateConfigUtils(): Promise<void> {
const configContent = [
'export const ServerConfig = {',
"export const ServerConfig = {",
` name: "${this.config.serverName}",`,
` version: "${this.config.serverVersion}",`,
` description: "${this.config.description}",`,
'} as const;',
'',
'export const EnvironmentVariables = {',
"} as const;",
"",
"export const EnvironmentVariables = {",
' ONEUPTIME_URL: "ONEUPTIME_URL",',
' ONEUPTIME_API_URL: "ONEUPTIME_API_URL",',
' ONEUPTIME_API_KEY: "ONEUPTIME_API_KEY",',
' API_KEY: "API_KEY",',
'} as const;',
'',
'export function validateEnvironment(): void {',
' const apiKey = process.env.ONEUPTIME_API_KEY || process.env.API_KEY;',
' ',
' if (!apiKey) {',
' throw new Error(',
"} as const;",
"",
"export function validateEnvironment(): void {",
" const apiKey = process.env.ONEUPTIME_API_KEY || process.env.API_KEY;",
" ",
" if (!apiKey) {",
" throw new Error(",
' "OneUptime API key is required. Please set one of the following environment variables:\\n" +',
' "- ONEUPTIME_API_KEY\\n" +',
' "- API_KEY"',
' );',
' }',
'}',
'',
'export function getEnvironmentInfo(): Record<string, string | undefined> {',
' return {',
' ONEUPTIME_URL: process.env.ONEUPTIME_URL,',
' ONEUPTIME_API_URL: process.env.ONEUPTIME_API_URL,',
" );",
" }",
"}",
"",
"export function getEnvironmentInfo(): Record<string, string | undefined> {",
" return {",
" ONEUPTIME_URL: process.env.ONEUPTIME_URL,",
" ONEUPTIME_API_URL: process.env.ONEUPTIME_API_URL,",
' ONEUPTIME_API_KEY: process.env.ONEUPTIME_API_KEY ? "[REDACTED]" : undefined,',
' API_KEY: process.env.API_KEY ? "[REDACTED]" : undefined,',
' NODE_ENV: process.env.NODE_ENV,',
' };',
'}',
].join('\n');
" NODE_ENV: process.env.NODE_ENV,",
" };",
"}",
].join("\n");
this.fileGenerator.ensureDirectoryExists("Utils");
await this.fileGenerator.writeFile("Utils/Config.ts", configContent);
@@ -396,14 +404,19 @@ export class MCPServerGenerator {
const toolList = tools
.slice(0, 20)
.map((tool) => `- **${tool.name}**: ${tool.description}`)
.map((tool) => {
return `- **${tool.name}**: ${tool.description}`;
})
.join("\n");
const resourceList = resourceTags
.map((tag) => `- **${StringUtils.toPascalCase(tag)}**`)
.map((tag) => {
return `- **${StringUtils.toPascalCase(tag)}**`;
})
.join("\n");
const additionalToolsNote = tools.length > 20 ? `\n...and ${tools.length - 20} more tools` : "";
const additionalToolsNote =
tools.length > 20 ? `\n...and ${tools.length - 20} more tools` : "";
const readmeContent = `# ${this.config.serverName}

View File

@@ -1,10 +1,5 @@
import fs from "fs";
import {
OpenAPISpec,
OpenAPIOperation,
MCPTool,
OpenAPISchema,
} from "./Types";
import { OpenAPISpec, OpenAPIOperation, MCPTool, OpenAPISchema } from "./Types";
import { StringUtils } from "./StringUtils";
export class OpenAPIParser {
@@ -36,7 +31,11 @@ export class OpenAPIParser {
// Group operations by resource/tag
for (const [path, pathItem] of Object.entries(this.spec.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (!operation.operationId || !operation.tags || operation.tags.length === 0) {
if (
!operation.operationId ||
!operation.tags ||
operation.tags.length === 0
) {
continue;
}
@@ -48,10 +47,17 @@ export class OpenAPIParser {
return tools;
}
private createMCPTool(path: string, method: string, operation: OpenAPIOperation): MCPTool {
private createMCPTool(
path: string,
method: string,
operation: OpenAPIOperation,
): MCPTool {
const toolName: string = this.generateToolName(operation);
const description: string = operation.description || operation.summary || `${method.toUpperCase()} ${path}`;
const description: string =
operation.description ||
operation.summary ||
`${method.toUpperCase()} ${path}`;
const inputSchema: any = this.generateInputSchema(operation);
return {
@@ -84,11 +90,17 @@ export class OpenAPIParser {
// Add path parameters
if (operation.parameters) {
for (const param of operation.parameters) {
if (param.in === "path" || param.in === "query" || param.in === "header") {
if (
param.in === "path" ||
param.in === "query" ||
param.in === "header"
) {
const paramName = StringUtils.toCamelCase(param.name);
properties[paramName] = this.convertOpenAPISchemaToJsonSchema(param.schema);
properties[paramName] = this.convertOpenAPISchemaToJsonSchema(
param.schema,
);
properties[paramName].description = param.description || "";
if (param.required || param.in === "path") {
required.push(paramName);
}
@@ -100,17 +112,23 @@ export class OpenAPIParser {
if (operation.requestBody) {
const content = operation.requestBody.content;
const jsonContent = content["application/json"];
if (jsonContent && jsonContent.schema) {
if (jsonContent.schema.properties) {
// Flatten the request body properties into the main properties
Object.assign(properties, this.convertOpenAPISchemaToJsonSchema(jsonContent.schema).properties);
Object.assign(
properties,
this.convertOpenAPISchemaToJsonSchema(jsonContent.schema)
.properties,
);
if (jsonContent.schema.required) {
required.push(...jsonContent.schema.required);
}
} else {
// If it's a reference or complex schema, add as 'data' property
properties.data = this.convertOpenAPISchemaToJsonSchema(jsonContent.schema);
properties.data = this.convertOpenAPISchemaToJsonSchema(
jsonContent.schema,
);
if (operation.requestBody.required) {
required.push("data");
}
@@ -154,7 +172,8 @@ export class OpenAPIParser {
if (schema.properties) {
jsonSchema.properties = {};
for (const [propName, propSchema] of Object.entries(schema.properties)) {
jsonSchema.properties[propName] = this.convertOpenAPISchemaToJsonSchema(propSchema);
jsonSchema.properties[propName] =
this.convertOpenAPISchemaToJsonSchema(propSchema);
}
}
@@ -172,7 +191,11 @@ export class OpenAPIParser {
// Handle #/components/schemas/SchemeName format
const refParts = ref.split("/");
if (refParts[0] === "#" && refParts[1] === "components" && refParts[2] === "schemas") {
if (
refParts[0] === "#" &&
refParts[1] === "components" &&
refParts[2] === "schemas"
) {
const schemaName = refParts[3];
if (schemaName && this.spec.components?.schemas?.[schemaName]) {
return this.spec.components.schemas[schemaName];
@@ -188,11 +211,13 @@ export class OpenAPIParser {
}
const tags = new Set<string>();
for (const [, pathItem] of Object.entries(this.spec.paths)) {
for (const [, operation] of Object.entries(pathItem)) {
if (operation.tags) {
operation.tags.forEach(tag => tags.add(tag));
operation.tags.forEach((tag) => {
return tags.add(tag);
});
}
}
}

View File

@@ -33,13 +33,17 @@ async function main(): Promise<void> {
// Step 4: Initialize MCP server generator
Logger.info("⚙️ Step 3: Initializing MCP server generator...");
const generator = new MCPServerGenerator({
outputDir: mcpDir,
serverName: "oneuptime-mcp",
serverVersion: "1.0.0",
npmPackageName: "@oneuptime/mcp-server",
description: "OneUptime Model Context Protocol (MCP) Server - Provides access to OneUptime APIs for LLMs",
}, apiSpec);
const generator = new MCPServerGenerator(
{
outputDir: mcpDir,
serverName: "oneuptime-mcp",
serverVersion: "1.0.0",
npmPackageName: "@oneuptime/mcp-server",
description:
"OneUptime Model Context Protocol (MCP) Server - Provides access to OneUptime APIs for LLMs",
},
apiSpec,
);
// Step 5: Generate MCP server
Logger.info("🏗️ Step 4: Generating MCP server files...");
@@ -57,7 +61,6 @@ async function main(): Promise<void> {
Logger.info(" 3. Set up your environment variables");
Logger.info(" 4. npm run build");
Logger.info(" 5. Test with npm start");
} catch (error) {
Logger.error("💥 MCP server generation failed:");
Logger.error(error instanceof Error ? error.message : "Unknown error");
@@ -157,7 +160,7 @@ All notable changes to the OneUptime MCP Server will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - ${new Date().toISOString().split('T')[0]}
## [1.0.0] - ${new Date().toISOString().split("T")[0]}
### Added
- Initial release of OneUptime MCP Server

View File

@@ -1083,19 +1083,17 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
fields.push(
` "${apiFieldName}": r.convertTerraformListToInterface(data.${fieldName}),`,
);
} else if (attr.type === "string" && attr.isComplexObject) {
// For complex object strings, parse JSON and convert to interface{}
fields.push(
` "${apiFieldName}": r.parseJSONField(data.${fieldName}),`,
);
} else {
if (attr.type === "string" && attr.isComplexObject) {
// For complex object strings, parse JSON and convert to interface{}
fields.push(
` "${apiFieldName}": r.parseJSONField(data.${fieldName}),`,
);
} else {
const value: string = this.getGoValueForTerraformType(
attr.type,
`data.${fieldName}`,
);
fields.push(` "${apiFieldName}": ${value},`);
}
const value: string = this.getGoValueForTerraformType(
attr.type,
`data.${fieldName}`,
);
fields.push(` "${apiFieldName}": ${value},`);
}
}
@@ -1199,7 +1197,6 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
isCreateMethod: boolean = false,
originalFieldName?: string,
): string {
switch (terraformType) {
case "string":
// Handle binary format fields (like base64 file content) specially
@@ -1213,16 +1210,15 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
// Preserve original value from the request since API doesn't return file content
${fieldName} = types.StringValue(original${StringUtils.toPascalCase(originalFieldName)}Value)
}`;
} else {
// In Read/Update methods, preserve existing value if not present in API response
// This prevents drift detection when API doesn't return binary content
return `if val, ok := ${responseValue}.(string); ok {
}
// In Read/Update methods, preserve existing value if not present in API response
// This prevents drift detection when API doesn't return binary content
return `if val, ok := ${responseValue}.(string); ok {
${fieldName} = types.StringValue(val)
} else {
// Keep existing value to prevent drift - API doesn't return binary content
// ${fieldName} value is already set from the existing state
}`;
}
} else if (isComplexObject) {
// For complex object strings, convert API object response to JSON string
return `if val, ok := ${responseValue}.(map[string]interface{}); ok {
@@ -1236,8 +1232,8 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
} else {
${fieldName} = types.StringNull()
}`;
} else {
return `if obj, ok := ${responseValue}.(map[string]interface{}); ok {
}
return `if obj, ok := ${responseValue}.(map[string]interface{}); ok {
// Handle ObjectID type responses
if val, ok := obj["_id"].(string); ok && val != "" {
${fieldName} = types.StringValue(val)
@@ -1251,7 +1247,7 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
} else {
${fieldName} = types.StringNull()
}`;
}
case "number":
return `if val, ok := ${responseValue}.(float64); ok {
${fieldName} = types.NumberValue(big.NewFloat(val))
@@ -1268,13 +1264,13 @@ func (r *${resourceTypeName}Resource) Delete(ctx context.Context, req resource.D
return `if val, ok := ${responseValue}.(bool); ok {
${fieldName} = types.BoolValue(val)
}`;
} else {
return `if val, ok := ${responseValue}.(bool); ok {
}
return `if val, ok := ${responseValue}.(bool); ok {
${fieldName} = types.BoolValue(val)
} else if ${responseValue} == nil {
${fieldName} = types.BoolNull()
}`;
}
case "map":
return `if val, ok := ${responseValue}.(map[string]interface{}); ok {
// Convert API response map to Terraform map
@@ -1418,18 +1414,22 @@ ${resourceFunctions}
private generateOriginalValueStorage(resource: TerraformResource): string {
const storage: string[] = [];
// Find binary format fields and store their original values
for (const [name, attr] of Object.entries(resource.schema)) {
if (attr.format === "binary") {
const sanitizedName: string = this.sanitizeAttributeName(name);
const fieldName: string = StringUtils.toPascalCase(sanitizedName);
storage.push(` // Store the original ${sanitizedName} value since API won't return it`);
storage.push(` original${fieldName}Value := data.${fieldName}.ValueString()`);
storage.push(
` // Store the original ${sanitizedName} value since API won't return it`,
);
storage.push(
` original${fieldName}Value := data.${fieldName}.ValueString()`,
);
storage.push(``);
}
}
return storage.join("\n");
}
}