feat(MCP): Introduce MCP types, logger, and integration into the application

- Added McpTypes.ts for defining JSON schema and tool information.
- Created ModelType.ts and OneUptimeOperation.ts enums for model types and operations.
- Implemented MCPLogger for structured logging in MCP servers.
- Integrated MCP routes into the main application index.
- Updated package.json to include @modelcontextprotocol/sdk dependency.
- Removed MCP-related configurations from Helm chart and Docker Compose files.
- Added Data Processing Agreement (DPA) page and route to the legal section.
- Updated legal.ejs to include a link to the new DPA page.
This commit is contained in:
Nawaz Dhandala
2026-03-03 11:34:46 +00:00
parent d617b73a5d
commit 560f45f3cc
34 changed files with 3573 additions and 305 deletions

View File

@@ -304,23 +304,6 @@ jobs:
max_attempts: 3
command: cd TestServer && npm install && npm run compile && npm run dep-check
compile-mcp:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- name: Compile MCP
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MCP && npm update @oneuptime/common && npm install && npm run compile && npm run dep-check
compile-mobile-app:
runs-on: ubuntu-latest
env:

View File

@@ -148,73 +148,6 @@ jobs:
git commit -m "Helm Chart Release ${{needs.read-version.outputs.major_minor}}"
git push origin master
mcp-docker-image-deploy:
needs: [generate-build-number, read-version]
runs-on: ubuntu-latest
env:
QEMU_CPU: max
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Docker Meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oneuptime/mcp
ghcr.io/oneuptime/mcp
tags: |
type=raw,value=release,enable=true
type=semver,value=${{needs.read-version.outputs.major_minor}},pattern={{version}},enable=true
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v10.0.4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate Dockerfile from Dockerfile.tpl
run: npm run prerun
# Build and deploy mcp.
- name: Login to Docker Hub
run: |
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Login to GitHub Container Registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
- name: Build and push
run: |
bash ./Scripts/GHA/build_docker_images.sh \
--image mcp \
--version "${{needs.read-version.outputs.major_minor}}" \
--dockerfile ./MCP/Dockerfile \
--context . \
--platforms linux/amd64,linux/arm64 \
--git-sha "${{ github.sha }}"
nginx-docker-image-deploy:
needs: [generate-build-number, read-version]
runs-on: ubuntu-latest
@@ -1546,7 +1479,6 @@ jobs:
needs:
- read-version
- generate-build-number
- mcp-docker-image-deploy
- nginx-docker-image-deploy
- e2e-docker-image-deploy
- isolated-vm-docker-image-deploy
@@ -1637,7 +1569,7 @@ jobs:
test-e2e-release-saas:
runs-on: ubuntu-latest
needs: [telemetry-docker-image-deploy, mcp-docker-image-deploy, docs-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
needs: [telemetry-docker-image-deploy, docs-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
@@ -1768,7 +1700,7 @@ jobs:
test-e2e-release-self-hosted:
runs-on: ubuntu-latest
# After all the jobs runs
needs: [telemetry-docker-image-deploy, mcp-docker-image-deploy, docs-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
needs: [telemetry-docker-image-deploy, docs-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:

View File

@@ -86,76 +86,6 @@ jobs:
echo "patch=${target_patch}" >> "$GITHUB_OUTPUT"
echo "Using version base: ${new_version}"
mcp-docker-image-deploy:
needs: [read-version, generate-build-number]
runs-on: ubuntu-latest
env:
QEMU_CPU: max
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Docker Meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oneuptime/mcp
ghcr.io/oneuptime/mcp
tags: |
type=raw,value=test,enable=true
type=raw,value=${{needs.read-version.outputs.major_minor}}-test,enable=true
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v10.0.4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate Dockerfile from Dockerfile.tpl
run: npm run prerun
# Build and deploy mcp.
- name: Login to Docker Hub
run: |
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Login to GitHub Container Registry
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
- name: Build and push
run: |
bash ./Scripts/GHA/build_docker_images.sh \
--image mcp \
--version "${{needs.read-version.outputs.major_minor}}-test" \
--dockerfile ./MCP/Dockerfile \
--context . \
--platforms linux/amd64,linux/arm64 \
--git-sha "${{ github.sha }}" \
--extra-tags test \
--extra-enterprise-tags enterprise-test
nginx-docker-image-deploy:
needs: [read-version, generate-build-number]
runs-on: ubuntu-latest
@@ -1442,7 +1372,7 @@ jobs:
test-helm-chart:
runs-on: ubuntu-latest
needs: [infrastructure-agent-deploy, mcp-docker-image-deploy, publish-terraform-provider, telemetry-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy]
needs: [infrastructure-agent-deploy, publish-terraform-provider, telemetry-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, ai-agent-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:

View File

@@ -1,21 +0,0 @@
name: MCP Server Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd MCP && npm install && npm run test

View File

@@ -0,0 +1,30 @@
/**
* Server Configuration
* Centralized configuration for the MCP server
*/
import { Host, HttpProtocol } from "Common/Server/EnvironmentConfig";
// Application name used across the server
export const APP_NAME: string = "mcp";
// MCP Server information
export const MCP_SERVER_NAME: string = "oneuptime-mcp";
export const MCP_SERVER_VERSION: string = "1.0.0";
// Route prefixes for the MCP server (only /mcp since App owns root)
export const ROUTE_PREFIXES: string[] = [`/${APP_NAME}`];
// API URL configuration
export function getApiUrl(): string {
return Host ? `${HttpProtocol}${Host}` : "https://oneuptime.com";
}
// Session header name
export const SESSION_HEADER: string = "mcp-session-id";
// API key header names
export const API_KEY_HEADERS: string[] = ["x-api-key", "authorization"];
// Response formatting limits
export const LIST_PREVIEW_LIMIT: number = 5;

View File

@@ -0,0 +1,232 @@
/**
* Route Handler
* Sets up Express routes for the MCP server
*/
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "node:crypto";
import {
ExpressApplication,
ExpressRequest,
ExpressResponse,
NextFunction,
ExpressJson,
} from "Common/Server/Utils/Express";
import { getMCPServer, McpServer } from "../Server/MCPServer";
import SessionManager, { SessionData } from "../Server/SessionManager";
import { McpToolInfo } from "../Types/McpTypes";
import {
ROUTE_PREFIXES,
SESSION_HEADER,
API_KEY_HEADERS,
} from "../Config/ServerConfig";
import logger from "Common/Server/Utils/Logger";
// Type for MCP handler function
type McpHandlerFunction = (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
) => Promise<void>;
/**
* Extract API key from request headers
*/
export function extractApiKey(req: ExpressRequest): string | undefined {
for (const header of API_KEY_HEADERS) {
const value: string | undefined = req.headers[header] as string | undefined;
if (value) {
// Handle Bearer token format
if (header === "authorization" && value.startsWith("Bearer ")) {
return value.replace("Bearer ", "");
}
return value;
}
}
return undefined;
}
/**
* Setup all MCP-specific routes on the Express app
*/
export function setupMCPRoutes(
app: ExpressApplication,
tools: McpToolInfo[],
): void {
ROUTE_PREFIXES.forEach((prefix: string) => {
setupRoutesForPrefix(app, prefix, tools);
});
logger.info(
`MCP routes setup complete for prefixes: ${ROUTE_PREFIXES.join(", ")}`,
);
}
/**
* Setup routes for a specific prefix
*/
function setupRoutesForPrefix(
app: ExpressApplication,
prefix: string,
tools: McpToolInfo[],
): void {
const mcpEndpoint: string = `${prefix}/mcp`;
const mcpHandler: McpHandlerFunction = createMCPHandler();
// MCP endpoint for all methods (GET for SSE, POST for requests, DELETE for cleanup)
app.get(mcpEndpoint, mcpHandler);
app.post(mcpEndpoint, ExpressJson(), mcpHandler);
app.delete(mcpEndpoint, mcpHandler);
// OPTIONS handler for CORS preflight requests
app.options(mcpEndpoint, (_req: ExpressRequest, res: ExpressResponse) => {
res.status(200).end();
});
// List tools endpoint (REST API)
setupToolsEndpoint(app, prefix, tools);
// Health check endpoint
setupHealthEndpoint(app, prefix, tools);
}
/**
* Create the main MCP request handler
*/
function createMCPHandler(): McpHandlerFunction {
return async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
// Extract API key (optional - public tools work without it)
const apiKey: string | undefined = extractApiKey(req);
// Set the current API key for tool calls (may be undefined for public tools)
SessionManager.setCurrentApiKey(apiKey || "");
// Check for existing session
const sessionId: string | undefined = req.headers[
SESSION_HEADER
] as string;
if (sessionId && SessionManager.hasSession(sessionId)) {
await handleExistingSession(req, res, sessionId, apiKey || "");
return;
}
// Create new session for new connections
await handleNewSession(req, res, apiKey || "");
} catch (error) {
next(error);
}
};
}
/**
* Handle request for an existing session
*/
async function handleExistingSession(
req: ExpressRequest,
res: ExpressResponse,
sessionId: string,
apiKey: string,
): Promise<void> {
const sessionData: SessionData | undefined =
SessionManager.getSession(sessionId);
if (!sessionData) {
return;
}
// Update API key in case it changed
sessionData.apiKey = apiKey;
await sessionData.transport.handleRequest(req, res, req.body);
}
/**
* Handle request for a new session (initialization)
*/
async function handleNewSession(
req: ExpressRequest,
res: ExpressResponse,
apiKey: string,
): Promise<void> {
const mcpServer: McpServer = getMCPServer();
const transport: StreamableHTTPServerTransport =
new StreamableHTTPServerTransport({
sessionIdGenerator: (): string => {
return randomUUID();
},
onsessioninitialized: (newSessionId: string): void => {
// Store the transport with the new session ID and API key
SessionManager.setSession(newSessionId, { transport, apiKey });
logger.info(`New MCP session initialized: ${newSessionId}`);
},
});
// Handle transport close
transport.onclose = (): void => {
const transportSessionId: string | undefined = transport.sessionId;
if (transportSessionId) {
logger.info(`MCP session closed: ${transportSessionId}`);
SessionManager.removeSession(transportSessionId);
}
};
// Handle transport errors
transport.onerror = (error: Error): void => {
logger.error(`MCP transport error: ${error.message}`);
};
// Connect the MCP server to this transport
await mcpServer.connect(transport as Parameters<typeof mcpServer.connect>[0]);
// Handle the request
await transport.handleRequest(req, res, req.body);
}
/**
* Setup the tools listing endpoint
*/
function setupToolsEndpoint(
app: ExpressApplication,
prefix: string,
tools: McpToolInfo[],
): void {
const endpoint: string = `${prefix}/tools`;
app.get(endpoint, (_req: ExpressRequest, res: ExpressResponse) => {
const toolsList: Array<{ name: string; description: string }> = tools.map(
(tool: McpToolInfo) => {
return {
name: tool.name,
description: tool.description,
};
},
);
res.json({ tools: toolsList, count: toolsList.length });
});
}
/**
* Setup the health check endpoint
*/
function setupHealthEndpoint(
app: ExpressApplication,
prefix: string,
tools: McpToolInfo[],
): void {
const endpoint: string = `${prefix}/health`;
app.get(endpoint, (_req: ExpressRequest, res: ExpressResponse) => {
res.json({
status: "healthy",
service: "oneuptime-mcp",
tools: tools.length,
activeSessions: SessionManager.getSessionCount(),
});
});
}

View File

@@ -0,0 +1,376 @@
/**
* Tool Handler
* Handles MCP tool execution and response formatting
*/
import {
CallToolRequestSchema,
CallToolRequest,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
McpToolInfo,
OneUptimeToolCallArgs,
JSONSchema,
} from "../Types/McpTypes";
import OneUptimeOperation from "../Types/OneUptimeOperation";
import OneUptimeApiService from "../Services/OneUptimeApiService";
import SessionManager from "../Server/SessionManager";
import { LIST_PREVIEW_LIMIT } from "../Config/ServerConfig";
import { isHelperTool, handleHelperTool } from "../Tools/HelperTools";
import {
isPublicStatusPageTool,
handlePublicStatusPageTool,
} from "../Tools/PublicStatusPageTools";
import logger from "Common/Server/Utils/Logger";
/**
* Register tool handlers on the MCP server
*/
export function registerToolHandlers(
mcpServer: McpServer,
tools: McpToolInfo[],
): void {
// Register list tools handler
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
return handleListTools(tools);
});
// Register call tool handler
mcpServer.server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest) => {
return handleCallTool(request, tools);
},
);
logger.info(`Registered handlers for ${tools.length} tools`);
}
/**
* Handle list tools request
*/
function handleListTools(tools: McpToolInfo[]): {
tools: Array<{ name: string; description: string; inputSchema: JSONSchema }>;
} {
const mcpTools: Array<{
name: string;
description: string;
inputSchema: JSONSchema;
}> = tools.map((tool: McpToolInfo) => {
return {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
};
});
logger.info(`Listing ${mcpTools.length} available tools`);
return { tools: mcpTools };
}
/**
* Handle tool call request
*/
async function handleCallTool(
request: CallToolRequest,
tools: McpToolInfo[],
): Promise<{ content: Array<{ type: string; text: string }> }> {
const { name, arguments: args } = request.params;
try {
// Check if this is a helper tool (doesn't require API key)
if (isHelperTool(name)) {
logger.info(`Executing helper tool: ${name}`);
const responseText: string = handleHelperTool(
name,
(args || {}) as Record<string, unknown>,
tools.filter((t: McpToolInfo) => {
return !isHelperTool(t.name) && !isPublicStatusPageTool(t.name);
}),
);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
// Check if this is a public status page tool (doesn't require API key)
if (isPublicStatusPageTool(name)) {
logger.info(`Executing public status page tool: ${name}`);
const responseText: string = await handlePublicStatusPageTool(
name,
(args || {}) as Record<string, unknown>,
);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
// Find the tool by name
const tool: McpToolInfo | undefined = tools.find((t: McpToolInfo) => {
return t.name === name;
});
if (!tool) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}. Use 'oneuptime_help' to see available tools.`,
);
}
logger.info(`Executing tool: ${name} for model: ${tool.modelName}`);
// Validate API key is available for this session
const apiKey: string = SessionManager.getCurrentApiKey();
if (!apiKey) {
throw new McpError(
ErrorCode.InvalidRequest,
"API key is required. Please provide x-api-key header in your request. Use 'oneuptime_help' to learn more.",
);
}
// Execute the OneUptime operation with the session's API key
const result: unknown = await OneUptimeApiService.executeOperation(
tool.tableName,
tool.operation,
tool.modelType,
tool.apiPath || "",
args as OneUptimeToolCallArgs,
apiKey,
);
// Format the response
const responseText: string = formatToolResponse(
tool,
result,
args as OneUptimeToolCallArgs,
);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
} catch (error) {
logger.error(`Error executing tool ${name}: ${error}`);
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Failed to execute ${name}: ${error}. Use 'oneuptime_help' for guidance.`,
);
}
}
/**
* Format tool response based on operation type
*/
export function formatToolResponse(
tool: McpToolInfo,
result: unknown,
args: OneUptimeToolCallArgs,
): string {
const operation: OneUptimeOperation = tool.operation;
const modelName: string = tool.singularName;
const pluralName: string = tool.pluralName;
switch (operation) {
case OneUptimeOperation.Create:
return formatCreateResponse(modelName, result);
case OneUptimeOperation.Read:
return formatReadResponse(modelName, result, args.id);
case OneUptimeOperation.List:
return formatListResponse(modelName, pluralName, result);
case OneUptimeOperation.Update:
return formatUpdateResponse(modelName, result, args.id);
case OneUptimeOperation.Delete:
return formatDeleteResponse(modelName, args.id);
case OneUptimeOperation.Count:
return formatCountResponse(pluralName, result);
default:
return `Operation ${operation} completed successfully: ${JSON.stringify(result, null, 2)}`;
}
}
function formatCreateResponse(modelName: string, result: unknown): string {
const response: Record<string, unknown> = {
success: true,
operation: "create",
resourceType: modelName,
message: `Successfully created ${modelName}`,
data: result,
};
return JSON.stringify(response, null, 2);
}
function formatReadResponse(
modelName: string,
result: unknown,
id: string | undefined,
): string {
if (result) {
const response: Record<string, unknown> = {
success: true,
operation: "read",
resourceType: modelName,
resourceId: id,
data: result,
};
return JSON.stringify(response, null, 2);
}
const response: Record<string, unknown> = {
success: false,
operation: "read",
resourceType: modelName,
resourceId: id,
error: `${modelName} not found with ID: ${id}`,
suggestion: `Use list_${modelName.toLowerCase().replace(/\s+/g, "_")}s to find valid IDs`,
};
return JSON.stringify(response, null, 2);
}
function formatListResponse(
modelName: string,
pluralName: string,
result: unknown,
): string {
const items: Array<unknown> = Array.isArray(result)
? result
: (result as { data?: Array<unknown> })?.data || [];
const count: number = items.length;
const response: Record<string, unknown> = {
success: true,
operation: "list",
resourceType: pluralName,
totalReturned: count,
hasMore: count >= LIST_PREVIEW_LIMIT,
message:
count === 0
? `No ${pluralName} found matching the criteria`
: `Found ${count} ${count === 1 ? modelName : pluralName}`,
data: items.slice(0, LIST_PREVIEW_LIMIT),
};
if (count > LIST_PREVIEW_LIMIT) {
response["note"] =
`Showing first ${LIST_PREVIEW_LIMIT} results. Use 'skip' parameter to paginate.`;
}
return JSON.stringify(response, null, 2);
}
function formatUpdateResponse(
modelName: string,
result: unknown,
id: string | undefined,
): string {
const response: Record<string, unknown> = {
success: true,
operation: "update",
resourceType: modelName,
resourceId: id,
message: `Successfully updated ${modelName}`,
data: result,
};
return JSON.stringify(response, null, 2);
}
function formatDeleteResponse(
modelName: string,
id: string | undefined,
): string {
const response: Record<string, unknown> = {
success: true,
operation: "delete",
resourceType: modelName,
resourceId: id,
message: `Successfully deleted ${modelName} (ID: ${id})`,
};
return JSON.stringify(response, null, 2);
}
function formatCountResponse(pluralName: string, result: unknown): string {
let totalCount: number = 0;
if (result !== null && result !== undefined) {
if (typeof result === "number") {
totalCount = result;
} else if (typeof result === "object") {
const resultObj: Record<string, unknown> = result as Record<
string,
unknown
>;
// Handle { count: number } format
if ("count" in resultObj) {
const countValue: unknown = resultObj["count"];
if (typeof countValue === "number") {
totalCount = countValue;
} else if (typeof countValue === "object" && countValue !== null) {
// Handle PositiveNumber or other objects with value/toNumber
const countObj: Record<string, unknown> = countValue as Record<
string,
unknown
>;
if (typeof countObj["value"] === "number") {
totalCount = countObj["value"];
} else if (
typeof (countObj as { toNumber?: () => number }).toNumber ===
"function"
) {
totalCount = (countObj as { toNumber: () => number }).toNumber();
}
}
}
// Handle { data: { count: number } } format
else if (
"data" in resultObj &&
typeof resultObj["data"] === "object" &&
resultObj["data"] !== null
) {
const dataObj: Record<string, unknown> = resultObj["data"] as Record<
string,
unknown
>;
if ("count" in dataObj && typeof dataObj["count"] === "number") {
totalCount = dataObj["count"];
}
}
}
}
const response: Record<string, unknown> = {
success: true,
operation: "count",
resourceType: pluralName,
count: totalCount,
message: `Total count of ${pluralName}: ${totalCount}`,
};
return JSON.stringify(response, null, 2);
}

View File

@@ -0,0 +1,51 @@
/**
* MCP FeatureSet
* Integrates the Model Context Protocol server into the App service
*/
import FeatureSet from "Common/Server/Types/FeatureSet";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import { getApiUrl } from "./Config/ServerConfig";
import { initializeMCPServer, getMCPServer } from "./Server/MCPServer";
import { registerToolHandlers } from "./Handlers/ToolHandler";
import { setupMCPRoutes } from "./Handlers/RouteHandler";
import { generateAllTools } from "./Tools/ToolGenerator";
import OneUptimeApiService, {
OneUptimeApiConfig,
} from "./Services/OneUptimeApiService";
import { McpToolInfo } from "./Types/McpTypes";
const MCPFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp();
// Initialize OneUptime API Service
const apiUrl: string = getApiUrl();
const config: OneUptimeApiConfig = {
url: apiUrl,
};
OneUptimeApiService.initialize(config);
logger.info(
`MCP: OneUptime API Service initialized with: ${apiUrl}`,
);
// Initialize MCP server
initializeMCPServer();
// Generate tools
const tools: McpToolInfo[] = generateAllTools();
logger.info(`MCP: Generated ${tools.length} tools`);
// Register tool handlers
registerToolHandlers(getMCPServer(), tools);
// Setup MCP-specific routes
setupMCPRoutes(app, tools);
logger.info(`MCP FeatureSet initialized successfully`);
},
};
export default MCPFeatureSet;

View File

@@ -0,0 +1,66 @@
/**
* MCP Server
* Handles MCP server initialization and configuration
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../Config/ServerConfig";
import logger from "Common/Server/Utils/Logger";
// Singleton MCP server instance
let mcpServerInstance: McpServer | null = null;
/**
* Initialize and return the MCP server instance
*/
export function initializeMCPServer(): McpServer {
if (mcpServerInstance) {
return mcpServerInstance;
}
mcpServerInstance = new McpServer(
{
name: MCP_SERVER_NAME,
version: MCP_SERVER_VERSION,
},
{
capabilities: {
tools: {},
},
},
);
logger.info(
`MCP Server initialized: ${MCP_SERVER_NAME} v${MCP_SERVER_VERSION}`,
);
return mcpServerInstance;
}
/**
* Get the MCP server instance
* @throws Error if server not initialized
*/
export function getMCPServer(): McpServer {
if (!mcpServerInstance) {
throw new Error(
"MCP Server not initialized. Call initializeMCPServer() first.",
);
}
return mcpServerInstance;
}
/**
* Check if MCP server is initialized
*/
export function isMCPServerInitialized(): boolean {
return mcpServerInstance !== null;
}
/**
* Reset MCP server (useful for testing)
*/
export function resetMCPServer(): void {
mcpServerInstance = null;
}
export { McpServer };

View File

@@ -0,0 +1,113 @@
/**
* Session Manager
* Manages MCP sessions and their associated data
*/
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import logger from "Common/Server/Utils/Logger";
// Session data interface
export interface SessionData {
transport: StreamableHTTPServerTransport;
apiKey: string;
}
/**
* SessionManager handles the lifecycle of MCP sessions
*/
export default class SessionManager {
private static sessions: Map<string, SessionData> = new Map();
private static currentSessionApiKey: string = "";
/**
* Get all active sessions
*/
public static getSessions(): Map<string, SessionData> {
return this.sessions;
}
/**
* Check if a session exists
*/
public static hasSession(sessionId: string): boolean {
return this.sessions.has(sessionId);
}
/**
* Get a session by ID
*/
public static getSession(sessionId: string): SessionData | undefined {
return this.sessions.get(sessionId);
}
/**
* Create or update a session
*/
public static setSession(sessionId: string, data: SessionData): void {
this.sessions.set(sessionId, data);
logger.info(`MCP session stored: ${sessionId}`);
}
/**
* Update the API key for an existing session
*/
public static updateSessionApiKey(
sessionId: string,
apiKey: string,
): boolean {
const session: SessionData | undefined = this.sessions.get(sessionId);
if (session) {
session.apiKey = apiKey;
return true;
}
return false;
}
/**
* Remove a session
*/
public static removeSession(sessionId: string): boolean {
const deleted: boolean = this.sessions.delete(sessionId);
if (deleted) {
logger.info(`MCP session removed: ${sessionId}`);
}
return deleted;
}
/**
* Get the current session API key (used during request processing)
*/
public static getCurrentApiKey(): string {
return this.currentSessionApiKey;
}
/**
* Set the current session API key (called at the start of each request)
*/
public static setCurrentApiKey(apiKey: string): void {
this.currentSessionApiKey = apiKey;
}
/**
* Clear the current session API key
*/
public static clearCurrentApiKey(): void {
this.currentSessionApiKey = "";
}
/**
* Get the count of active sessions
*/
public static getSessionCount(): number {
return this.sessions.size;
}
/**
* Clear all sessions (useful for cleanup)
*/
public static clearAllSessions(): void {
this.sessions.clear();
this.currentSessionApiKey = "";
logger.info("All MCP sessions cleared");
}
}

View File

@@ -0,0 +1,378 @@
/**
* OneUptime API Service
* Handles communication with the OneUptime API
*/
import OneUptimeOperation from "../Types/OneUptimeOperation";
import ModelType from "../Types/ModelType";
import { OneUptimeToolCallArgs } from "../Types/McpTypes";
import { generateAllFieldsSelect } from "./SelectFieldGenerator";
import MCPLogger from "../Utils/MCPLogger";
import API from "Common/Utils/API";
import URL from "Common/Types/API/URL";
import Route from "Common/Types/API/Route";
import Headers from "Common/Types/API/Headers";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import { JSONObject, JSONValue } from "Common/Types/JSON";
import Protocol from "Common/Types/API/Protocol";
import Hostname from "Common/Types/API/Hostname";
export interface OneUptimeApiConfig {
url: string;
apiKey?: string;
}
export default class OneUptimeApiService {
private static api: API;
/**
* Initialize the API service with configuration
*/
public static initialize(config: OneUptimeApiConfig): void {
try {
const url: URL = URL.fromString(config.url);
const protocol: Protocol = url.protocol;
const hostname: Hostname = url.hostname;
this.api = new API(protocol, hostname, new Route("/"));
} catch (error) {
throw new Error(`Invalid URL format: ${config.url}. Error: ${error}`);
}
MCPLogger.info(`OneUptime API Service initialized with: ${config.url}`);
}
/**
* Execute a OneUptime operation
*/
public static async executeOperation(
tableName: string,
operation: OneUptimeOperation,
modelType: ModelType,
apiPath: string,
args: OneUptimeToolCallArgs,
apiKey: string,
): Promise<JSONValue> {
this.validateInitialization();
this.validateApiKey(apiKey);
this.validateOperationArgs(operation, args);
const route: Route = this.buildApiRoute(apiPath, operation, args.id);
const headers: Headers = this.buildHeaders(apiKey);
const data: JSONObject | undefined = this.buildRequestData(
operation,
args,
tableName,
modelType,
);
MCPLogger.info(
`Executing ${operation} operation for ${tableName} at ${route.toString()}`,
);
try {
const response: JSONValue = await this.makeApiRequest(
operation,
route,
headers,
data,
);
MCPLogger.info(
`Successfully executed ${operation} operation for ${tableName}`,
);
return response;
} catch (error) {
MCPLogger.error(
`Error executing ${operation} operation for ${tableName}: ${error}`,
);
throw error;
}
}
/**
* Make the actual API request
*/
private static async makeApiRequest(
operation: OneUptimeOperation,
route: Route,
headers: Headers,
data: JSONObject | undefined,
): Promise<JSONValue> {
const url: URL = new URL(this.api.protocol, this.api.hostname, route);
const baseOptions: { url: URL; headers: Headers } = { url, headers };
let response: HTTPResponse<JSONObject> | HTTPErrorResponse;
switch (operation) {
case OneUptimeOperation.Create:
case OneUptimeOperation.Count:
case OneUptimeOperation.List:
case OneUptimeOperation.Read:
response = await API.post(
data ? { ...baseOptions, data } : baseOptions,
);
break;
case OneUptimeOperation.Update:
response = await API.put(data ? { ...baseOptions, data } : baseOptions);
break;
case OneUptimeOperation.Delete:
response = await API.delete(
data ? { ...baseOptions, data } : baseOptions,
);
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
}
if (response instanceof HTTPErrorResponse) {
throw new Error(
`API request failed: ${response.statusCode} - ${response.message}`,
);
}
return response.data;
}
/**
* Build the API route for an operation
*/
private static buildApiRoute(
apiPath: string,
operation: OneUptimeOperation,
id?: string,
): Route {
let fullPath: string = `/api${apiPath}`;
switch (operation) {
case OneUptimeOperation.Read:
if (id) {
fullPath = `/api${apiPath}/${id}/get-item`;
}
break;
case OneUptimeOperation.Update:
case OneUptimeOperation.Delete:
if (id) {
fullPath = `/api${apiPath}/${id}/`;
}
break;
case OneUptimeOperation.Count:
fullPath = `/api${apiPath}/count`;
break;
case OneUptimeOperation.List:
fullPath = `/api${apiPath}/get-list`;
break;
case OneUptimeOperation.Create:
default:
fullPath = `/api${apiPath}`;
break;
}
return new Route(fullPath);
}
/**
* Build request data based on operation type
*/
private static buildRequestData(
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:
return this.buildCreateData(args);
case OneUptimeOperation.Update:
return this.buildUpdateData(args);
case OneUptimeOperation.List:
case OneUptimeOperation.Count:
return this.buildQueryData(args, tableName, modelType);
case OneUptimeOperation.Read:
return this.buildReadData(args, tableName, modelType);
case OneUptimeOperation.Delete:
default:
return undefined;
}
}
private static buildCreateData(args: OneUptimeToolCallArgs): JSONObject {
const createData: JSONObject = {};
const reservedFields: string[] = [
"id",
"query",
"select",
"skip",
"limit",
"sort",
];
for (const [key, value] of Object.entries(args)) {
if (!reservedFields.includes(key)) {
createData[key] = value as JSONValue;
}
}
return { data: createData } as JSONObject;
}
private static buildUpdateData(args: OneUptimeToolCallArgs): JSONObject {
const updateData: JSONObject = {};
const reservedFields: string[] = [
"id",
"query",
"select",
"skip",
"limit",
"sort",
];
for (const [key, value] of Object.entries(args)) {
if (!reservedFields.includes(key)) {
updateData[key] = value as JSONValue;
}
}
return { data: updateData } as JSONObject;
}
private static buildQueryData(
args: OneUptimeToolCallArgs,
tableName: string,
modelType: ModelType,
): JSONObject {
const generatedSelect: JSONObject =
args.select || generateAllFieldsSelect(tableName, modelType);
const requestData: JSONObject = {
query: args.query || {},
select: generatedSelect,
skip: args.skip,
limit: args.limit,
sort: args.sort,
} as JSONObject;
MCPLogger.info(`Request data: ${JSON.stringify(requestData, null, 2)}`);
return requestData;
}
private static buildReadData(
args: OneUptimeToolCallArgs,
tableName: string,
modelType: ModelType,
): JSONObject {
const readSelect: JSONObject =
args.select || generateAllFieldsSelect(tableName, modelType);
const readRequestData: JSONObject = {
select: readSelect,
} as JSONObject;
MCPLogger.info(
`Request data for Read: ${JSON.stringify(readRequestData, null, 2)}`,
);
return readRequestData;
}
/**
* Build headers for API request
*/
private static buildHeaders(apiKey: string): Headers {
return {
"Content-Type": "application/json",
Accept: "application/json",
APIKey: apiKey,
};
}
/**
* Validate that the service is initialized
*/
private static validateInitialization(): void {
if (!this.api) {
throw new Error(
"OneUptime API Service not initialized. Please call initialize() first.",
);
}
}
/**
* Validate that an API key is provided
*/
private static validateApiKey(apiKey: string): void {
if (!apiKey) {
throw new Error(
"API key is required. Please provide x-api-key header in your request.",
);
}
}
/**
* Validate arguments for a specific operation
*/
public static validateOperationArgs(
operation: OneUptimeOperation,
args: OneUptimeToolCallArgs,
): void {
const reservedFields: string[] = [
"id",
"query",
"select",
"skip",
"limit",
"sort",
];
switch (operation) {
case OneUptimeOperation.Create: {
const createDataFields: string[] = Object.keys(args).filter(
(key: string) => {
return !reservedFields.includes(key);
},
);
if (createDataFields.length === 0) {
throw new Error(
"At least one data field is required for create operation",
);
}
break;
}
case OneUptimeOperation.Read:
case OneUptimeOperation.Delete:
if (!args.id) {
throw new Error(`ID is required for ${operation} operation`);
}
break;
case OneUptimeOperation.Update: {
if (!args.id) {
throw new Error(`ID is required for ${operation} operation`);
}
const updateDataFields: string[] = Object.keys(args).filter(
(key: string) => {
return !reservedFields.includes(key);
},
);
if (updateDataFields.length === 0) {
throw new Error(
"At least one data field is required for update operation",
);
}
break;
}
case OneUptimeOperation.List:
case OneUptimeOperation.Count:
// No required arguments for list/count operations
break;
default:
throw new Error(`Unknown operation: ${operation}`);
}
}
}

View File

@@ -0,0 +1,304 @@
/**
* Select Field Generator
* Generates select field objects for API queries
*/
import DatabaseModels from "Common/Models/DatabaseModels/Index";
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { ModelSchema } from "Common/Utils/Schema/ModelSchema";
import { AnalyticsModelSchema } from "Common/Utils/Schema/AnalyticsModelSchema";
import { getTableColumns } from "Common/Types/Database/TableColumn";
import Permission from "Common/Types/Permission";
import ModelType from "../Types/ModelType";
import MCPLogger from "../Utils/MCPLogger";
import { JSONObject } from "Common/Types/JSON";
// Type for model constructor
type ModelConstructor<T> = new () => T;
// Type for model class with table name
interface ModelWithTableName {
tableName: string;
getColumnAccessControlForAllColumns?: () => Record<
string,
ColumnAccessControl
>;
}
// Type for column access control
interface ColumnAccessControl {
read?: Permission[];
create?: Permission[];
update?: Permission[];
}
// Type for table columns
type TableColumns = Record<string, unknown>;
// Type for Zod schema shape
interface ZodSchemaWithShape {
_def?: {
shape?: Record<string, unknown> | (() => Record<string, unknown>);
};
}
/**
* Generate a select object that includes all fields from the select schema
*/
export function generateAllFieldsSelect(
tableName: string,
modelType: ModelType,
): JSONObject {
MCPLogger.info(
`Generating select for tableName: ${tableName}, modelType: ${modelType}`,
);
try {
const ModelClass:
| ModelConstructor<BaseModel>
| ModelConstructor<AnalyticsBaseModel>
| null = findModelClass(tableName, modelType);
if (!ModelClass) {
MCPLogger.warn(
`Model class not found for ${tableName}, using empty select`,
);
return {};
}
MCPLogger.info(
`Found ModelClass: ${(ModelClass as { name: string }).name} for tableName: ${tableName}`,
);
// Try to get raw table columns first (most reliable approach)
const selectFromColumns: JSONObject | null = generateSelectFromTableColumns(
ModelClass as ModelConstructor<BaseModel>,
tableName,
);
if (selectFromColumns && Object.keys(selectFromColumns).length > 0) {
return selectFromColumns;
}
// Fallback to schema approach if table columns fail
return generateSelectFromSchema(ModelClass, tableName, modelType);
} catch (error) {
MCPLogger.error(`Error generating select for ${tableName}: ${error}`);
return getDefaultSelect();
}
}
/**
* Find the model class by table name
*/
function findModelClass(
tableName: string,
modelType: ModelType,
): ModelConstructor<BaseModel> | ModelConstructor<AnalyticsBaseModel> | null {
if (modelType === ModelType.Database) {
MCPLogger.info(`Searching DatabaseModels for tableName: ${tableName}`);
return (
(DatabaseModels.find((Model: ModelConstructor<BaseModel>): boolean => {
try {
const instance: ModelWithTableName =
new Model() as unknown as ModelWithTableName;
const instanceTableName: string = instance.tableName;
MCPLogger.info(
`Checking model ${Model.name} with tableName: ${instanceTableName}`,
);
return instanceTableName === tableName;
} catch (error) {
MCPLogger.warn(`Error instantiating model ${Model.name}: ${error}`);
return false;
}
}) as ModelConstructor<BaseModel> | undefined) || null
);
}
if (modelType === ModelType.Analytics) {
MCPLogger.info(`Searching AnalyticsModels for tableName: ${tableName}`);
return (
(AnalyticsModels.find(
(Model: ModelConstructor<AnalyticsBaseModel>): boolean => {
try {
const instance: ModelWithTableName =
new Model() as unknown as ModelWithTableName;
return instance.tableName === tableName;
} catch (error) {
MCPLogger.warn(
`Error instantiating analytics model ${Model.name}: ${error}`,
);
return false;
}
},
) as ModelConstructor<AnalyticsBaseModel> | undefined) || null
);
}
return null;
}
/**
* Generate select object from table columns
*/
function generateSelectFromTableColumns(
ModelClass: ModelConstructor<BaseModel>,
tableName: string,
): JSONObject | null {
try {
const modelInstance: ModelWithTableName =
new ModelClass() as unknown as ModelWithTableName;
const tableColumns: TableColumns = getTableColumns(
modelInstance as BaseModel,
);
const columnNames: string[] = Object.keys(tableColumns);
MCPLogger.info(
`Raw table columns (${columnNames.length}): ${columnNames.slice(0, 10).join(", ")}`,
);
if (columnNames.length === 0) {
return null;
}
// Get access control information to filter out restricted fields
const accessControlForColumns: Record<string, ColumnAccessControl> =
modelInstance.getColumnAccessControlForAllColumns
? modelInstance.getColumnAccessControlForAllColumns()
: {};
const selectObject: JSONObject = {};
let filteredCount: number = 0;
for (const columnName of columnNames) {
if (shouldIncludeField(columnName, accessControlForColumns)) {
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)`,
);
// 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`);
return getDefaultSelect();
}
return selectObject;
} catch (tableColumnError) {
MCPLogger.warn(
`Failed to get table columns for ${tableName}: ${tableColumnError}`,
);
return null;
}
}
/**
* Check if a field should be included based on access control
*/
function shouldIncludeField(
columnName: string,
accessControlForColumns: Record<string, ColumnAccessControl>,
): boolean {
const accessControl: ColumnAccessControl | undefined =
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
*/
return (
!accessControl ||
(accessControl.read !== undefined &&
accessControl.read.length > 0 &&
!(
accessControl.read.length === 1 &&
accessControl.read[0] === Permission.CurrentUser
))
);
}
/**
* Generate select object from model schema (fallback)
*/
function generateSelectFromSchema(
ModelClass:
| ModelConstructor<BaseModel>
| ModelConstructor<AnalyticsBaseModel>,
tableName: string,
modelType: ModelType,
): JSONObject {
let selectSchema: ZodSchemaWithShape;
if (modelType === ModelType.Database) {
MCPLogger.info(
`Generating select schema for database model: ${(ModelClass as { name: string }).name}`,
);
selectSchema = ModelSchema.getSelectModelSchema({
modelType: ModelClass as ModelConstructor<BaseModel>,
}) as ZodSchemaWithShape;
} else {
MCPLogger.info(
`Generating schema for analytics model: ${(ModelClass as { name: string }).name}`,
);
selectSchema = AnalyticsModelSchema.getModelSchema({
modelType: ModelClass as ModelConstructor<AnalyticsBaseModel>,
}) as ZodSchemaWithShape;
}
// Extract field names from the schema
const selectObject: JSONObject = {};
const rawShape:
| Record<string, unknown>
| (() => Record<string, unknown>)
| undefined = selectSchema._def?.shape;
// Handle both function and object shapes
const shape: Record<string, unknown> | undefined =
typeof rawShape === "function" ? rawShape() : rawShape;
MCPLogger.info(`Schema shape keys: ${shape ? Object.keys(shape).length : 0}`);
if (shape) {
const fieldNames: string[] = Object.keys(shape);
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`,
);
// 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}`);
return getDefaultSelect();
}
return selectObject;
}
/**
* Get default select fields
*/
function getDefaultSelect(): JSONObject {
return {
_id: true,
createdAt: true,
updatedAt: true,
};
}

View File

@@ -0,0 +1,392 @@
/**
* Helper Tools
* Provides utility tools for agents to discover and understand OneUptime MCP capabilities
*/
import { McpToolInfo } from "../Types/McpTypes";
import OneUptimeOperation from "../Types/OneUptimeOperation";
import ModelType from "../Types/ModelType";
export interface ResourceInfo {
name: string;
singularName: string;
pluralName: string;
description: string;
operations: string[];
}
/**
* Generate helper tools for MCP
*/
export function generateHelperTools(
resourceTools: McpToolInfo[],
): McpToolInfo[] {
// Extract unique resources from tools
const resources: Map<string, ResourceInfo> = new Map();
for (const tool of resourceTools) {
if (!resources.has(tool.tableName)) {
resources.set(tool.tableName, {
name: tool.tableName,
singularName: tool.singularName,
pluralName: tool.pluralName,
description: getResourceDescription(tool.singularName),
operations: [],
});
}
const resource: ResourceInfo | undefined = resources.get(tool.tableName);
if (resource) {
resource.operations.push(tool.operation);
}
}
const resourceList: ResourceInfo[] = Array.from(resources.values());
return [createHelpTool(resourceList), createResourceInfoTool(resourceList)];
}
function getResourceDescription(singularName: string): string {
const descriptions: Record<string, string> = {
Incident:
"Represents service disruptions or issues affecting your systems. Track incident lifecycle from creation to resolution.",
Monitor:
"Defines what to monitor (websites, APIs, servers) and how to check their health and availability.",
Alert:
"Notifications triggered when monitors detect issues. Configures who gets notified and how.",
Project:
"Top-level container for all your monitoring resources. Organizes monitors, incidents, and team members.",
"Status Page":
"Public-facing page showing the status of your services to your customers.",
"Scheduled Maintenance":
"Planned downtime events that inform users about expected service interruptions.",
Team: "Groups of users with shared access to project resources.",
"On-Call Duty Policy":
"Defines escalation rules and schedules for incident response.",
"Incident State":
"Represents the lifecycle states of incidents (e.g., Created, Acknowledged, Resolved).",
"Monitor Status":
"Represents the health states of monitors (e.g., Operational, Degraded, Offline).",
};
return (
descriptions[singularName] ||
`Manages ${singularName} resources in OneUptime.`
);
}
function createHelpTool(resources: ResourceInfo[]): McpToolInfo {
const resourceSummary: string = resources
.map((r: ResourceInfo) => {
return `- ${r.pluralName}: ${r.description}`;
})
.join("\n");
return {
name: "oneuptime_help",
description: `Get help and guidance for using the OneUptime MCP server. Returns information about available resources and common operations. Use this tool first to understand what you can do with OneUptime.
AVAILABLE RESOURCES:
${resourceSummary}
COMMON WORKFLOWS:
1. List incidents: Use list_incidents to see current incidents
2. Create incident: Use create_incident with title and severity
3. Check monitor status: Use list_monitors to see all monitors and their status
4. Count resources: Use count_* tools to get totals without fetching all data`,
inputSchema: {
type: "object",
properties: {
topic: {
type: "string",
description:
"Optional topic to get help on: 'resources', 'incidents', 'monitors', 'alerts', 'workflows', or 'examples'",
enum: [
"resources",
"incidents",
"monitors",
"alerts",
"workflows",
"examples",
],
},
},
additionalProperties: false,
},
modelName: "Help",
operation: OneUptimeOperation.Read,
modelType: ModelType.Database,
singularName: "Help",
pluralName: "Help",
tableName: "Help",
apiPath: "",
};
}
function createResourceInfoTool(_resources: ResourceInfo[]): McpToolInfo {
return {
name: "oneuptime_list_resources",
description:
"List all available OneUptime resources and their supported operations. Use this to discover what resources you can manage through the MCP server.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
modelName: "ResourceInfo",
operation: OneUptimeOperation.List,
modelType: ModelType.Database,
singularName: "Resource",
pluralName: "Resources",
tableName: "ResourceInfo",
apiPath: "",
};
}
/**
* Handle helper tool execution
*/
export function handleHelperTool(
toolName: string,
args: Record<string, unknown>,
resourceTools: McpToolInfo[],
): string {
// Extract unique resources from tools
const resources: Map<string, ResourceInfo> = new Map();
for (const tool of resourceTools) {
if (!resources.has(tool.tableName)) {
resources.set(tool.tableName, {
name: tool.tableName,
singularName: tool.singularName,
pluralName: tool.pluralName,
description: getResourceDescription(tool.singularName),
operations: [],
});
}
const resource: ResourceInfo | undefined = resources.get(tool.tableName);
if (resource) {
resource.operations.push(tool.operation);
}
}
const resourceList: ResourceInfo[] = Array.from(resources.values());
if (toolName === "oneuptime_help") {
return handleHelpTool(args, resourceList);
} else if (toolName === "oneuptime_list_resources") {
return handleListResourcesTool(resourceList);
}
return JSON.stringify({ error: "Unknown helper tool" });
}
function handleHelpTool(
args: Record<string, unknown>,
resourceList: ResourceInfo[],
): string {
const topic: string = (args["topic"] as string) || "general";
const response: Record<string, unknown> = {
success: true,
topic,
data: {} as Record<string, unknown>,
};
switch (topic) {
case "resources":
(response["data"] as Record<string, unknown>)["resources"] =
resourceList.map((r: ResourceInfo) => {
return {
name: r.name,
singularName: r.singularName,
pluralName: r.pluralName,
description: r.description,
availableOperations: r.operations,
};
});
(response["data"] as Record<string, unknown>)["hint"] =
"Use the specific tool for each operation. For example: list_incidents, create_incident, get_incident, update_incident, delete_incident, count_incidents";
break;
case "incidents":
(response["data"] as Record<string, unknown>)["description"] =
"Incidents represent service disruptions or issues. They have states (Created, Acknowledged, Resolved) and severities.";
(response["data"] as Record<string, unknown>)["commonOperations"] = [
{
tool: "list_incidents",
description:
"List all incidents, optionally filtered by state or severity",
},
{
tool: "create_incident",
description: "Create a new incident when an issue is detected",
},
{
tool: "update_incident",
description: "Update incident state, severity, or add notes",
},
{
tool: "count_incidents",
description: "Get count of incidents by state",
},
];
(response["data"] as Record<string, unknown>)["example"] = {
createIncident: {
title: "Database connection failure",
description: "Production database is not responding to queries",
incidentSeverityId: "<severity-uuid>",
},
};
break;
case "monitors":
(response["data"] as Record<string, unknown>)["description"] =
"Monitors check the health and availability of your services (websites, APIs, servers).";
(response["data"] as Record<string, unknown>)["commonOperations"] = [
{
tool: "list_monitors",
description: "List all monitors and their current status",
},
{
tool: "create_monitor",
description: "Create a new monitor to watch a service",
},
{
tool: "update_monitor",
description: "Update monitor configuration or enable/disable",
},
{ tool: "count_monitors", description: "Get total number of monitors" },
];
break;
case "alerts":
(response["data"] as Record<string, unknown>)["description"] =
"Alerts are notifications sent when monitors detect issues.";
(response["data"] as Record<string, unknown>)["commonOperations"] = [
{
tool: "list_alerts",
description: "List all alerts and their status",
},
{ tool: "count_alerts", description: "Get count of alerts" },
];
break;
case "workflows":
(response["data"] as Record<string, unknown>)["workflows"] = [
{
name: "Check system status",
steps: [
"1. Use count_incidents to see if there are active incidents",
"2. Use list_monitors with query to find any monitors with issues",
"3. Use list_incidents to get details of any active incidents",
],
},
{
name: "Create and manage incident",
steps: [
"1. Use list_incident_states to get available states",
"2. Use create_incident with title, description, and severity",
"3. Use update_incident to change state as incident progresses",
],
},
{
name: "Incident summary report",
steps: [
"1. Use count_incidents to get total count",
"2. Use list_incidents with sort by createdAt descending",
"3. Group and summarize the results",
],
},
];
break;
case "examples":
(response["data"] as Record<string, unknown>)["examples"] = {
listRecentIncidents: {
tool: "list_incidents",
args: { limit: 10, sort: { createdAt: -1 } },
},
countActiveIncidents: {
tool: "count_incidents",
args: { query: {} },
},
getSpecificIncident: {
tool: "get_incident",
args: { id: "<incident-uuid>" },
},
updateIncidentTitle: {
tool: "update_incident",
args: { id: "<incident-uuid>", title: "Updated title" },
},
};
break;
default:
(response["data"] as Record<string, unknown>)["welcome"] =
"Welcome to OneUptime MCP Server!";
(response["data"] as Record<string, unknown>)["description"] =
"OneUptime is an open-source monitoring platform. This MCP server lets you manage incidents, monitors, alerts, and more.";
(response["data"] as Record<string, unknown>)["availableTopics"] = [
"resources",
"incidents",
"monitors",
"alerts",
"workflows",
"examples",
];
(response["data"] as Record<string, unknown>)["quickStart"] = [
"1. Use 'oneuptime_list_resources' to see all available resources",
"2. Use 'list_*' tools to browse existing data",
"3. Use 'count_*' tools to get quick summaries",
"4. Use 'create_*' tools to add new items",
];
(response["data"] as Record<string, unknown>)["resourceCount"] =
resourceList.length;
break;
}
return JSON.stringify(response, null, 2);
}
function handleListResourcesTool(resources: ResourceInfo[]): string {
const response: Record<string, unknown> = {
success: true,
totalResources: resources.length,
resources: resources.map((r: ResourceInfo) => {
return {
name: r.name,
singularName: r.singularName,
pluralName: r.pluralName,
description: r.description,
operations: r.operations,
tools: {
create: `create_${r.singularName.toLowerCase().replace(/\s+/g, "_")}`,
get: `get_${r.singularName.toLowerCase().replace(/\s+/g, "_")}`,
list: `list_${r.pluralName.toLowerCase().replace(/\s+/g, "_")}`,
update: `update_${r.singularName.toLowerCase().replace(/\s+/g, "_")}`,
delete: `delete_${r.singularName.toLowerCase().replace(/\s+/g, "_")}`,
count: `count_${r.pluralName.toLowerCase().replace(/\s+/g, "_")}`,
},
};
}),
};
return JSON.stringify(response, null, 2);
}
/**
* Check if a tool is a helper tool (doesn't require API key)
*/
export function isHelperTool(toolName: string): boolean {
return (
toolName === "oneuptime_help" || toolName === "oneuptime_list_resources"
);
}
/**
* Check if a tool doesn't require API key (helper or public status page tool)
*/
export function isPublicTool(toolName: string): boolean {
// Import check is done in ToolHandler to avoid circular dependencies
return isHelperTool(toolName);
}

View File

@@ -0,0 +1,415 @@
/**
* Public Status Page Tools
* Provides tools for querying public status pages without authentication
* These tools can be used with either a status page ID or domain name
*/
import { McpToolInfo, JSONSchema } from "../Types/McpTypes";
import OneUptimeOperation from "../Types/OneUptimeOperation";
import ModelType from "../Types/ModelType";
import MCPLogger from "../Utils/MCPLogger";
import API from "Common/Utils/API";
import URL from "Common/Types/API/URL";
import Route from "Common/Types/API/Route";
import Headers from "Common/Types/API/Headers";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import { JSONObject } from "Common/Types/JSON";
import { getApiUrl } from "../Config/ServerConfig";
// Common input schema for status page identifier
const statusPageIdentifierSchema: JSONSchema = {
type: "object",
properties: {
statusPageIdOrDomain: {
type: "string",
description:
"The status page ID (UUID) or domain name (e.g., 'status.company.com'). Use domain for public status pages with custom domains.",
},
},
required: ["statusPageIdOrDomain"],
additionalProperties: false,
};
/**
* Generate public status page tools
*/
export function generatePublicStatusPageTools(): McpToolInfo[] {
return [
createGetOverviewTool(),
createGetIncidentsTool(),
createGetScheduledMaintenanceTool(),
createGetAnnouncementsTool(),
];
}
function createGetOverviewTool(): McpToolInfo {
return {
name: "get_public_status_page_overview",
description: `Get the complete overview of a public status page including current status, resources, active incidents, scheduled maintenance, and announcements.
This tool does NOT require an API key and works with public status pages.
USAGE:
- By domain: statusPageIdOrDomain = "status.company.com"
- By ID: statusPageIdOrDomain = "550e8400-e29b-41d4-a716-446655440000"
RETURNS:
- Status page metadata (name, description, branding)
- Resources and their current status
- Active incidents
- Upcoming scheduled maintenance
- Active announcements
- Monitor status history`,
inputSchema: statusPageIdentifierSchema,
modelName: "StatusPageOverview",
operation: OneUptimeOperation.Read,
modelType: ModelType.Database,
singularName: "Status Page Overview",
pluralName: "Status Page Overviews",
tableName: "StatusPageOverview",
apiPath: "/status-page",
};
}
function createGetIncidentsTool(): McpToolInfo {
return {
name: "get_public_status_page_incidents",
description: `Get incidents from a public status page.
This tool does NOT require an API key and works with public status pages.
USAGE:
- By domain: statusPageIdOrDomain = "status.company.com"
- By ID: statusPageIdOrDomain = "550e8400-e29b-41d4-a716-446655440000"
RETURNS:
- List of incidents (active and recent history)
- Incident details (title, description, severity)
- Incident timeline and state changes
- Public notes/updates for each incident`,
inputSchema: {
type: "object",
properties: {
statusPageIdOrDomain: {
type: "string",
description:
"The status page ID (UUID) or domain name (e.g., 'status.company.com')",
},
incidentId: {
type: "string",
description: "Optional: Specific incident ID to fetch details for",
},
},
required: ["statusPageIdOrDomain"],
additionalProperties: false,
},
modelName: "StatusPageIncidents",
operation: OneUptimeOperation.List,
modelType: ModelType.Database,
singularName: "Status Page Incident",
pluralName: "Status Page Incidents",
tableName: "StatusPageIncidents",
apiPath: "/status-page",
};
}
function createGetScheduledMaintenanceTool(): McpToolInfo {
return {
name: "get_public_status_page_scheduled_maintenance",
description: `Get scheduled maintenance events from a public status page.
This tool does NOT require an API key and works with public status pages.
USAGE:
- By domain: statusPageIdOrDomain = "status.company.com"
- By ID: statusPageIdOrDomain = "550e8400-e29b-41d4-a716-446655440000"
RETURNS:
- List of scheduled maintenance events (upcoming and ongoing)
- Maintenance details (title, description, scheduled times)
- Maintenance timeline and state changes
- Public notes/updates for each maintenance event`,
inputSchema: {
type: "object",
properties: {
statusPageIdOrDomain: {
type: "string",
description:
"The status page ID (UUID) or domain name (e.g., 'status.company.com')",
},
scheduledMaintenanceId: {
type: "string",
description:
"Optional: Specific scheduled maintenance ID to fetch details for",
},
},
required: ["statusPageIdOrDomain"],
additionalProperties: false,
},
modelName: "StatusPageScheduledMaintenance",
operation: OneUptimeOperation.List,
modelType: ModelType.Database,
singularName: "Status Page Scheduled Maintenance",
pluralName: "Status Page Scheduled Maintenances",
tableName: "StatusPageScheduledMaintenance",
apiPath: "/status-page",
};
}
function createGetAnnouncementsTool(): McpToolInfo {
return {
name: "get_public_status_page_announcements",
description: `Get announcements from a public status page.
This tool does NOT require an API key and works with public status pages.
USAGE:
- By domain: statusPageIdOrDomain = "status.company.com"
- By ID: statusPageIdOrDomain = "550e8400-e29b-41d4-a716-446655440000"
RETURNS:
- List of active announcements
- Announcement details (title, description, dates)`,
inputSchema: {
type: "object",
properties: {
statusPageIdOrDomain: {
type: "string",
description:
"The status page ID (UUID) or domain name (e.g., 'status.company.com')",
},
announcementId: {
type: "string",
description:
"Optional: Specific announcement ID to fetch details for",
},
},
required: ["statusPageIdOrDomain"],
additionalProperties: false,
},
modelName: "StatusPageAnnouncements",
operation: OneUptimeOperation.List,
modelType: ModelType.Database,
singularName: "Status Page Announcement",
pluralName: "Status Page Announcements",
tableName: "StatusPageAnnouncements",
apiPath: "/status-page",
};
}
/**
* Check if a tool is a public status page tool
*/
export function isPublicStatusPageTool(toolName: string): boolean {
return (
toolName === "get_public_status_page_overview" ||
toolName === "get_public_status_page_incidents" ||
toolName === "get_public_status_page_scheduled_maintenance" ||
toolName === "get_public_status_page_announcements"
);
}
/**
* Handle public status page tool execution
*/
export async function handlePublicStatusPageTool(
toolName: string,
args: Record<string, unknown>,
): Promise<string> {
const statusPageIdOrDomain: string = args["statusPageIdOrDomain"] as string;
if (!statusPageIdOrDomain) {
return JSON.stringify({
success: false,
error: "statusPageIdOrDomain is required",
});
}
try {
switch (toolName) {
case "get_public_status_page_overview":
return await getStatusPageOverview(statusPageIdOrDomain);
case "get_public_status_page_incidents":
return await getStatusPageIncidents(
statusPageIdOrDomain,
args["incidentId"] as string | undefined,
);
case "get_public_status_page_scheduled_maintenance":
return await getStatusPageScheduledMaintenance(
statusPageIdOrDomain,
args["scheduledMaintenanceId"] as string | undefined,
);
case "get_public_status_page_announcements":
return await getStatusPageAnnouncements(
statusPageIdOrDomain,
args["announcementId"] as string | undefined,
);
default:
return JSON.stringify({
success: false,
error: `Unknown public status page tool: ${toolName}`,
});
}
} catch (error) {
MCPLogger.error(
`Error executing public status page tool ${toolName}: ${error}`,
);
return JSON.stringify({
success: false,
error: `Failed to execute ${toolName}: ${error}`,
});
}
}
/**
* Get status page overview
* The backend now accepts both statusPageId and domain directly
*/
async function getStatusPageOverview(
statusPageIdOrDomain: string,
): Promise<string> {
const response: JSONObject = await makeStatusPageApiRequest(
"POST",
`/api/status-page/overview/${statusPageIdOrDomain}`,
);
return JSON.stringify(
{
success: true,
operation: "get_overview",
statusPageIdOrDomain,
data: response,
},
null,
2,
);
}
/**
* Get status page incidents
* The backend now accepts both statusPageId and domain directly
*/
async function getStatusPageIncidents(
statusPageIdOrDomain: string,
incidentId?: string,
): Promise<string> {
let route: string = `/api/status-page/incidents/${statusPageIdOrDomain}`;
if (incidentId) {
route = `/api/status-page/incidents/${statusPageIdOrDomain}/${incidentId}`;
}
const response: JSONObject = await makeStatusPageApiRequest("POST", route);
return JSON.stringify(
{
success: true,
operation: "get_incidents",
statusPageIdOrDomain,
incidentId: incidentId || null,
data: response,
},
null,
2,
);
}
/**
* Get status page scheduled maintenance events
* The backend now accepts both statusPageId and domain directly
*/
async function getStatusPageScheduledMaintenance(
statusPageIdOrDomain: string,
scheduledMaintenanceId?: string,
): Promise<string> {
let route: string = `/api/status-page/scheduled-maintenance-events/${statusPageIdOrDomain}`;
if (scheduledMaintenanceId) {
route = `/api/status-page/scheduled-maintenance-events/${statusPageIdOrDomain}/${scheduledMaintenanceId}`;
}
const response: JSONObject = await makeStatusPageApiRequest("POST", route);
return JSON.stringify(
{
success: true,
operation: "get_scheduled_maintenance",
statusPageIdOrDomain,
scheduledMaintenanceId: scheduledMaintenanceId || null,
data: response,
},
null,
2,
);
}
/**
* Get status page announcements
* The backend now accepts both statusPageId and domain directly
*/
async function getStatusPageAnnouncements(
statusPageIdOrDomain: string,
announcementId?: string,
): Promise<string> {
let route: string = `/api/status-page/announcements/${statusPageIdOrDomain}`;
if (announcementId) {
route = `/api/status-page/announcements/${statusPageIdOrDomain}/${announcementId}`;
}
const response: JSONObject = await makeStatusPageApiRequest("POST", route);
return JSON.stringify(
{
success: true,
operation: "get_announcements",
statusPageIdOrDomain,
announcementId: announcementId || null,
data: response,
},
null,
2,
);
}
/**
* Make a request to the StatusPage API
*/
async function makeStatusPageApiRequest(
method: "GET" | "POST",
path: string,
data?: JSONObject,
): Promise<JSONObject> {
const apiUrl: string = getApiUrl();
const url: URL = URL.fromString(apiUrl);
const route: Route = new Route(path);
const fullUrl: URL = new URL(url.protocol, url.hostname, route);
const headers: Headers = {
"Content-Type": "application/json",
Accept: "application/json",
};
MCPLogger.info(`Making ${method} request to ${fullUrl.toString()}`);
let response: HTTPResponse<JSONObject> | HTTPErrorResponse;
if (method === "GET") {
response = await API.get({ url: fullUrl, headers });
} else {
response = await API.post({ url: fullUrl, headers, data: data || {} });
}
if (response instanceof HTTPErrorResponse) {
MCPLogger.error(
`API request failed: ${response.statusCode} - ${response.message}`,
);
throw new Error(
`API request failed: ${response.statusCode} - ${response.message}`,
);
}
return response.data as JSONObject;
}

View File

@@ -0,0 +1,257 @@
/**
* Schema Converter
* Converts Zod schemas to JSON Schema format for MCP tools
*/
import { JSONSchemaProperty } from "../Types/McpTypes";
import { ModelSchemaType } from "Common/Utils/Schema/ModelSchema";
import { AnalyticsModelSchemaType } from "Common/Utils/Schema/AnalyticsModelSchema";
// Type for Zod field definition
interface ZodFieldDef {
typeName?: string;
innerType?: ZodField;
description?: string;
openapi?: {
metadata?: OpenApiMetadata;
};
}
// Type for Zod field
interface ZodField {
_def?: ZodFieldDef;
}
// Type for OpenAPI metadata
interface OpenApiMetadata {
type?: string;
description?: string;
example?: unknown;
format?: string;
default?: unknown;
items?: JSONSchemaProperty;
}
// Type for Zod schema with shape
interface ZodSchemaWithShape {
_def?: {
shape?: () => Record<string, ZodField>;
};
}
// Result type for schema conversion
export interface ZodToJsonSchemaResult {
type: string;
properties: Record<string, JSONSchemaProperty>;
required?: string[];
additionalProperties: boolean;
}
/**
* Convert a Zod schema to JSON Schema format for MCP tools
*/
export function zodToJsonSchema(
zodSchema: ModelSchemaType | AnalyticsModelSchemaType,
): ZodToJsonSchemaResult {
try {
const schemaWithShape: ZodSchemaWithShape =
zodSchema as unknown as ZodSchemaWithShape;
const shapeFunction: (() => Record<string, ZodField>) | undefined =
schemaWithShape._def?.shape;
if (!shapeFunction) {
return createEmptySchema();
}
const shape: Record<string, ZodField> = shapeFunction();
const properties: Record<string, JSONSchemaProperty> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
const { property, isRequired } = convertZodField(key, value);
properties[key] = property;
if (isRequired) {
required.push(key);
}
}
const result: ZodToJsonSchemaResult = {
type: "object",
properties,
additionalProperties: false,
};
if (required.length > 0) {
result.required = required;
}
return result;
} catch {
return createEmptySchema();
}
}
/**
* Convert a single Zod field to JSON Schema property
*/
function convertZodField(
key: string,
zodField: ZodField,
): { property: JSONSchemaProperty; isRequired: boolean } {
// Handle ZodOptional fields by looking at the inner type
let actualField: ZodField = zodField;
let isOptional: boolean = false;
if (zodField._def?.typeName === "ZodOptional") {
actualField = zodField._def.innerType || zodField;
isOptional = true;
}
// Extract OpenAPI metadata
const openApiMetadata: OpenApiMetadata | undefined =
actualField._def?.openapi?.metadata || zodField._def?.openapi?.metadata;
// Clean up description
const rawDescription: string =
zodField._def?.description ||
openApiMetadata?.description ||
`${key} field`;
const cleanDescription: string = cleanFieldDescription(rawDescription);
let property: JSONSchemaProperty;
if (openApiMetadata) {
property = buildPropertyFromMetadata(
openApiMetadata,
key,
cleanDescription,
);
} else {
// Fallback for fields without OpenAPI metadata
property = {
type: "string",
description: cleanDescription,
};
}
return {
property,
isRequired: !isOptional,
};
}
/**
* Build JSON Schema property from OpenAPI metadata
*/
function buildPropertyFromMetadata(
metadata: OpenApiMetadata,
key: string,
description: string,
): JSONSchemaProperty {
const property: JSONSchemaProperty = {
type: metadata.type || "string",
description,
};
// Add optional fields if present
if (metadata.example !== undefined) {
(property as JSONSchemaProperty & { example: unknown }).example =
metadata.example;
}
if (metadata.format) {
property.format = metadata.format;
}
if (metadata.default !== undefined) {
property.default = metadata.default;
}
// Handle array types
if (metadata.type === "array") {
property.items = metadata.items || {
type: "string",
description: `${key} item`,
};
}
return property;
}
/**
* Clean up description by removing permission information
*/
export function cleanFieldDescription(description: string): string {
if (!description) {
return description;
}
// Remove everything after ". Permissions -"
const permissionsIndex: number = description.indexOf(". Permissions -");
if (permissionsIndex !== -1) {
const beforeText: string = description.substring(0, permissionsIndex);
return addPeriodIfNeeded(beforeText);
}
// Handle cases where it starts with "Permissions -" without a preceding sentence
const permissionsStartIndex: number = description.indexOf("Permissions -");
if (permissionsStartIndex !== -1) {
const beforePermissions: string = description
.substring(0, permissionsStartIndex)
.trim();
if (beforePermissions && beforePermissions.length > 0) {
return addPeriodIfNeeded(beforePermissions);
}
}
return description;
}
/**
* Add period to text if it doesn't end with punctuation
*/
function addPeriodIfNeeded(text: string): string {
if (!text) {
return text;
}
const punctuation: string[] = [".", "!", "?"];
const lastChar: string = text.charAt(text.length - 1);
if (punctuation.includes(lastChar)) {
return text;
}
return text + ".";
}
/**
* Create an empty schema result
*/
function createEmptySchema(): ZodToJsonSchemaResult {
return {
type: "object",
properties: {},
additionalProperties: false,
};
}
/**
* Sanitize a name to be valid for MCP tool names
* MCP tool names can only contain [a-z0-9_-]
*/
export function sanitizeToolName(name: string): string {
return (
name
// Convert camelCase to snake_case
.replace(/([a-z])([A-Z])/g, "$1_$2")
.toLowerCase()
// Replace non-alphanumeric characters with underscores
.replace(/[^a-z0-9]/g, "_")
// Replace multiple consecutive underscores with single underscore
.replace(/_+/g, "_")
// Remove leading/trailing underscores
.replace(/^_|_$/g, "")
);
}

View File

@@ -0,0 +1,541 @@
/**
* Tool Generator
* Generates MCP tools for OneUptime models
*/
import DatabaseModels from "Common/Models/DatabaseModels/Index";
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
import DatabaseBaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { ModelSchema, ModelSchemaType } from "Common/Utils/Schema/ModelSchema";
import {
AnalyticsModelSchema,
AnalyticsModelSchemaType,
} from "Common/Utils/Schema/AnalyticsModelSchema";
import { McpToolInfo, ModelToolsResult } from "../Types/McpTypes";
import OneUptimeOperation from "../Types/OneUptimeOperation";
import ModelType from "../Types/ModelType";
import {
zodToJsonSchema,
sanitizeToolName,
ZodToJsonSchemaResult,
} from "./SchemaConverter";
import { generateHelperTools } from "./HelperTools";
import { generatePublicStatusPageTools } from "./PublicStatusPageTools";
import MCPLogger from "../Utils/MCPLogger";
/**
* Generate all MCP tools for all OneUptime models
*/
export function generateAllTools(): McpToolInfo[] {
const allTools: McpToolInfo[] = [];
// Generate tools for Database Models
const databaseTools: McpToolInfo[] = generateDatabaseModelTools();
allTools.push(...databaseTools);
// Generate tools for Analytics Models
const analyticsTools: McpToolInfo[] = generateAnalyticsModelTools();
allTools.push(...analyticsTools);
// Generate helper tools for discovery and guidance
const helperTools: McpToolInfo[] = generateHelperTools(allTools);
allTools.push(...helperTools);
// Generate public status page tools (no API key required)
const publicStatusPageTools: McpToolInfo[] = generatePublicStatusPageTools();
allTools.push(...publicStatusPageTools);
MCPLogger.info(
`Generated ${allTools.length} MCP tools for OneUptime models (including ${helperTools.length} helper tools and ${publicStatusPageTools.length} public status page tools)`,
);
return allTools;
}
/**
* Generate tools for all database models
*/
function generateDatabaseModelTools(): McpToolInfo[] {
const tools: McpToolInfo[] = [];
for (const ModelClass of DatabaseModels) {
try {
const model: DatabaseBaseModel = new ModelClass();
const result: ModelToolsResult = generateToolsForDatabaseModel(
model,
ModelClass,
);
tools.push(...result.tools);
} catch (error) {
MCPLogger.error(
`Error generating tools for database model ${ModelClass.name}: ${error}`,
);
}
}
return tools;
}
/**
* Generate tools for all analytics models
*/
function generateAnalyticsModelTools(): McpToolInfo[] {
const tools: McpToolInfo[] = [];
for (const ModelClass of AnalyticsModels) {
try {
const model: AnalyticsBaseModel = new ModelClass();
const result: ModelToolsResult = generateToolsForAnalyticsModel(
model,
ModelClass,
);
tools.push(...result.tools);
} catch (error) {
MCPLogger.error(
`Error generating tools for analytics model ${ModelClass.name}: ${error}`,
);
}
}
return tools;
}
/**
* Generate MCP tools for a specific database model
*/
export function generateToolsForDatabaseModel(
model: DatabaseBaseModel,
ModelClass: { new (): DatabaseBaseModel },
): ModelToolsResult {
const modelName: string = model.tableName || ModelClass.name;
const singularName: string = model.singularName || modelName;
const pluralName: string = model.pluralName || `${singularName}s`;
const apiPath: string | undefined = model.crudApiPath?.toString();
const modelInfo: ModelToolsResult["modelInfo"] = {
tableName: modelName,
singularName,
pluralName,
modelType: ModelType.Database,
...(apiPath && { apiPath }),
};
// Skip if model doesn't have required properties or MCP is disabled
if (!modelName || !model.enableMCP || !apiPath) {
return { tools: [], modelInfo };
}
// 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 tools: McpToolInfo[] = [
createCreateTool(
modelName,
singularName,
pluralName,
apiPath,
createSchema,
),
createReadTool(modelName, singularName, pluralName, apiPath),
createListTool(
modelName,
singularName,
pluralName,
apiPath,
querySchema,
sortSchema,
),
createUpdateTool(
modelName,
singularName,
pluralName,
apiPath,
updateSchema,
),
createDeleteTool(modelName, singularName, pluralName, apiPath),
createCountTool(modelName, singularName, pluralName, apiPath, querySchema),
];
return { tools, modelInfo };
}
/**
* Generate MCP tools for a specific analytics model
*/
export function generateToolsForAnalyticsModel(
model: AnalyticsBaseModel,
ModelClass: { new (): AnalyticsBaseModel },
): ModelToolsResult {
const modelName: string = model.tableName || ModelClass.name;
const singularName: string = model.singularName || modelName;
const pluralName: string = model.pluralName || `${singularName}s`;
const apiPath: string | undefined = model.crudApiPath?.toString();
const modelInfo: ModelToolsResult["modelInfo"] = {
tableName: modelName,
singularName,
pluralName,
modelType: ModelType.Analytics,
apiPath,
};
// Skip if model doesn't have required properties or MCP is disabled
if (!modelName || !model.enableMCP || !apiPath) {
return { tools: [], modelInfo };
}
// 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 tools: McpToolInfo[] = [
createAnalyticsCreateTool(
modelName,
singularName,
pluralName,
apiPath,
createSchema,
),
createAnalyticsListTool(
modelName,
singularName,
pluralName,
apiPath,
querySchema,
selectSchema,
sortSchema,
),
createAnalyticsCountTool(
modelName,
singularName,
pluralName,
apiPath,
querySchema,
),
];
return { tools, modelInfo };
}
// Database Model Tool Creators
function createCreateTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
createSchema: ModelSchemaType,
): McpToolInfo {
const schemaProperties: ZodToJsonSchemaResult = zodToJsonSchema(createSchema);
return {
name: `create_${sanitizeToolName(singularName)}`,
description: `Create a new ${singularName} in OneUptime. Returns the created ${singularName} object with its ID and all fields. Use this to add new ${pluralName} to your project.`,
inputSchema: {
type: "object",
properties: schemaProperties.properties || {},
required: schemaProperties.required || [],
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Create,
modelType: ModelType.Database,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createReadTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
): McpToolInfo {
return {
name: `get_${sanitizeToolName(singularName)}`,
description: `Retrieve a single ${singularName} by its unique ID from OneUptime. Returns the complete ${singularName} object with all its fields. Use list_${sanitizeToolName(pluralName)} first if you need to find the ID.`,
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: `The unique identifier (UUID) of the ${singularName} to retrieve. Example: "550e8400-e29b-41d4-a716-446655440000"`,
},
},
required: ["id"],
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Read,
modelType: ModelType.Database,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createListTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
querySchema: ModelSchemaType,
sortSchema: ModelSchemaType,
): McpToolInfo {
return {
name: `list_${sanitizeToolName(pluralName)}`,
description: `List and search ${pluralName} from OneUptime with optional filtering, pagination, and sorting. Returns an array of ${singularName} objects. Use the 'query' parameter to filter results by specific field values. Supports pagination via 'skip' and 'limit' parameters.`,
inputSchema: {
type: "object",
properties: {
query: {
...zodToJsonSchema(querySchema),
description: `Filter criteria for ${pluralName}. Each field can be used to filter results. Example: {"title": "My ${singularName}"} to find by title.`,
},
skip: {
type: "number",
description:
"Number of records to skip for pagination. Default: 0. Example: skip=10 to start from the 11th record.",
},
limit: {
type: "number",
description:
"Maximum number of records to return. Default: 10, Maximum: 100. Example: limit=25 to get 25 records.",
},
sort: {
...zodToJsonSchema(sortSchema),
description: `Sort order for results. Use 1 for ascending, -1 for descending. Example: {"createdAt": -1} to sort by newest first.`,
},
},
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.List,
modelType: ModelType.Database,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createUpdateTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
updateSchema: ModelSchemaType,
): McpToolInfo {
const schemaProperties: ZodToJsonSchemaResult = zodToJsonSchema(updateSchema);
return {
name: `update_${sanitizeToolName(singularName)}`,
description: `Update an existing ${singularName} in OneUptime. Only include the fields you want to change - unspecified fields will remain unchanged. Returns the updated ${singularName} object.`,
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: `The unique identifier (UUID) of the ${singularName} to update. Required. Use list_${sanitizeToolName(pluralName)} to find IDs.`,
},
...(schemaProperties.properties || {}),
},
required: ["id"],
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Update,
modelType: ModelType.Database,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createDeleteTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
): McpToolInfo {
return {
name: `delete_${sanitizeToolName(singularName)}`,
description: `Permanently delete a ${singularName} from OneUptime. This action cannot be undone. Returns a confirmation message upon successful deletion.`,
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: `The unique identifier (UUID) of the ${singularName} to delete. This action is irreversible.`,
},
},
required: ["id"],
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Delete,
modelType: ModelType.Database,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createCountTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
querySchema: ModelSchemaType,
): McpToolInfo {
return {
name: `count_${sanitizeToolName(pluralName)}`,
description: `Count the total number of ${pluralName} in OneUptime, optionally filtered by query criteria. Returns a single number. Useful for dashboards, reports, or checking if records exist before listing.`,
inputSchema: {
type: "object",
properties: {
query: {
...zodToJsonSchema(querySchema),
description: `Optional filter criteria. If omitted, counts all ${pluralName}. Example: {"currentIncidentStateId": "..."} to count incidents in a specific state.`,
},
},
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Count,
modelType: ModelType.Database,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
// Analytics Model Tool Creators
function createAnalyticsCreateTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
createSchema: AnalyticsModelSchemaType,
): McpToolInfo {
const schemaProperties: ZodToJsonSchemaResult = zodToJsonSchema(createSchema);
return {
name: `create_${sanitizeToolName(singularName)}`,
description: `Create a new ${singularName} analytics record in OneUptime`,
inputSchema: {
type: "object",
properties: schemaProperties.properties || {},
required: schemaProperties.required || [],
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Create,
modelType: ModelType.Analytics,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createAnalyticsListTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
querySchema: AnalyticsModelSchemaType,
selectSchema: AnalyticsModelSchemaType,
sortSchema: AnalyticsModelSchemaType,
): McpToolInfo {
return {
name: `list_${sanitizeToolName(pluralName)}`,
description: `Query ${pluralName} analytics data from OneUptime`,
inputSchema: {
type: "object",
properties: {
query: zodToJsonSchema(querySchema),
select: zodToJsonSchema(selectSchema),
skip: {
type: "number",
description: "Number of records to skip",
},
limit: {
type: "number",
description: "Maximum number of records to return",
},
sort: zodToJsonSchema(sortSchema),
},
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.List,
modelType: ModelType.Analytics,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}
function createAnalyticsCountTool(
modelName: string,
singularName: string,
pluralName: string,
apiPath: string,
querySchema: AnalyticsModelSchemaType,
): McpToolInfo {
return {
name: `count_${sanitizeToolName(pluralName)}`,
description: `Count ${pluralName} analytics records in OneUptime`,
inputSchema: {
type: "object",
properties: {
query: zodToJsonSchema(querySchema),
},
additionalProperties: false,
},
modelName,
operation: OneUptimeOperation.Count,
modelType: ModelType.Analytics,
singularName,
pluralName,
tableName: modelName,
apiPath,
};
}

View File

@@ -0,0 +1,68 @@
import OneUptimeOperation from "./OneUptimeOperation";
import ModelType from "./ModelType";
import { JSONObject } from "Common/Types/JSON";
// JSON Schema type for MCP tool input schemas
export interface JSONSchemaProperty {
type: string;
description?: string;
enum?: Array<string | number | boolean>;
items?: JSONSchemaProperty;
properties?: Record<string, JSONSchemaProperty>;
required?: string[];
default?: unknown;
format?: string;
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
}
export interface JSONSchema {
type: string;
properties?: Record<string, JSONSchemaProperty>;
required?: string[];
additionalProperties?: boolean;
description?: string;
}
export interface McpToolInfo {
name: string;
description: string;
inputSchema: JSONSchema;
modelName: string;
operation: OneUptimeOperation;
modelType: ModelType;
singularName: string;
pluralName: string;
tableName: string;
apiPath?: string;
}
export interface ModelToolsResult {
tools: McpToolInfo[];
modelInfo: {
tableName: string;
singularName: string;
pluralName: string;
modelType: ModelType;
apiPath?: string;
};
}
// Sort direction type
export type SortDirection = 1 | -1;
// Sort object type
export type SortObject = Record<string, SortDirection>;
export interface OneUptimeToolCallArgs {
id?: string;
data?: JSONObject;
query?: JSONObject;
select?: JSONObject;
skip?: number;
limit?: number;
sort?: SortObject;
}

View File

@@ -0,0 +1,6 @@
export enum ModelType {
Database = "database",
Analytics = "analytics",
}
export default ModelType;

View File

@@ -0,0 +1,10 @@
export enum OneUptimeOperation {
Create = "create",
Read = "read",
List = "list",
Update = "update",
Delete = "delete",
Count = "count",
}
export default OneUptimeOperation;

View File

@@ -0,0 +1,84 @@
/**
* MCP Logger - A logger specifically designed for MCP servers
* All logs are directed to stderr to avoid interfering with the JSON-RPC protocol on stdout
*/
import { LogLevel } from "Common/Server/EnvironmentConfig";
import ConfigLogLevel from "Common/Server/Types/ConfigLogLevel";
import { JSONObject } from "Common/Types/JSON";
import Exception from "Common/Types/Exception/Exception";
export type LogBody = string | JSONObject | Exception | Error | unknown;
export default class MCPLogger {
public static getLogLevel(): ConfigLogLevel {
if (!LogLevel) {
return ConfigLogLevel.INFO;
}
return LogLevel;
}
public static serializeLogBody(body: LogBody): string {
if (typeof body === "string") {
return body;
} else if (body instanceof Exception || body instanceof Error) {
return body.message;
}
return JSON.stringify(body);
}
public static info(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (logLevel === ConfigLogLevel.DEBUG || logLevel === ConfigLogLevel.INFO) {
// Use stderr instead of stdout for MCP compatibility
process.stderr.write(`[INFO] ${this.serializeLogBody(message)}\n`);
}
}
public static error(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (
logLevel === ConfigLogLevel.DEBUG ||
logLevel === ConfigLogLevel.INFO ||
logLevel === ConfigLogLevel.WARN ||
logLevel === ConfigLogLevel.ERROR
) {
// Use stderr for error messages
process.stderr.write(`[ERROR] ${this.serializeLogBody(message)}\n`);
}
}
public static warn(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (
logLevel === ConfigLogLevel.DEBUG ||
logLevel === ConfigLogLevel.INFO ||
logLevel === ConfigLogLevel.WARN
) {
// Use stderr for warning messages
process.stderr.write(`[WARN] ${this.serializeLogBody(message)}\n`);
}
}
public static debug(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (logLevel === ConfigLogLevel.DEBUG) {
// Use stderr for debug messages
process.stderr.write(`[DEBUG] ${this.serializeLogBody(message)}\n`);
}
}
public static trace(message: LogBody): void {
const logLevel: ConfigLogLevel = this.getLogLevel();
if (logLevel === ConfigLogLevel.DEBUG) {
// Use stderr for trace messages
process.stderr.write(`[TRACE] ${this.serializeLogBody(message)}\n`);
}
}
}

View File

@@ -1,6 +1,7 @@
import BaseAPIRoutes from "./FeatureSet/BaseAPI/Index";
// import FeatureSets.
import IdentityRoutes from "./FeatureSet/Identity/Index";
import MCPRoutes from "./FeatureSet/MCP/Index";
import NotificationRoutes from "./FeatureSet/Notification/Index";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import { ClickhouseAppInstance } from "Common/Server/Infrastructure/ClickhouseDatabase";
@@ -94,6 +95,7 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
await IdentityRoutes.init();
await NotificationRoutes.init();
await BaseAPIRoutes.init();
await MCPRoutes.init();
// Add default routes to the app
await App.addDefaultRoutes();

View File

@@ -20,6 +20,7 @@
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.0",
"@sendgrid/mail": "^8.1.0",
"Common": "file:../Common",
"ejs": "^3.1.9",

View File

@@ -131,9 +131,6 @@ Usage:
value: {{ $.Release.Name }}-admin-dashboard.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
- name: SERVER_DOCS_HOSTNAME
value: {{ $.Release.Name }}-docs.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
- name: SERVER_MCP_HOSTNAME
value: {{ $.Release.Name }}-mcp.{{ $.Release.Namespace }}.svc.{{ $.Values.global.clusterDomain }}
- name: APP_PORT
value: {{ $.Values.app.ports.http | squote }}
- name: TELEMETRY_PORT
@@ -158,8 +155,6 @@ Usage:
value: {{ $.Values.adminDashboard.ports.http | squote }}
- name: DOCS_PORT
value: {{ $.Values.docs.ports.http | squote }}
- name: MCP_PORT
value: {{ $.Values.mcp.ports.http | squote }}
{{- end }}

View File

@@ -1,19 +0,0 @@
{{- if $.Values.mcp.enabled }}
# OneUptime MCP Deployment
{{- $mcpEnv := dict "PORT" $.Values.mcp.ports.http "DISABLE_TELEMETRY" $.Values.mcp.disableTelemetryCollection -}}
{{- $mcpPorts := $.Values.mcp.ports -}}
{{- $mcpDeploymentArgs := dict "ServiceName" "mcp" "Ports" $mcpPorts "Release" $.Release "Values" $.Values "Env" $mcpEnv "Resources" $.Values.mcp.resources "NodeSelector" $.Values.mcp.nodeSelector "PodSecurityContext" $.Values.mcp.podSecurityContext "ContainerSecurityContext" $.Values.mcp.containerSecurityContext "DisableAutoscaler" $.Values.mcp.disableAutoscaler "ReplicaCount" $.Values.mcp.replicaCount -}}
{{- include "oneuptime.deployment" $mcpDeploymentArgs }}
---
# OneUptime MCP autoscaler
{{- $mcpAutoScalerArgs := dict "ServiceName" "mcp" "Release" $.Release "Values" $.Values "DisableAutoscaler" $.Values.mcp.disableAutoscaler -}}
{{- include "oneuptime.autoscaler" $mcpAutoScalerArgs }}
---
{{- end }}
# OneUptime MCP Service
{{- $mcpPorts := $.Values.mcp.ports -}}
{{- $mcpServiceArgs := dict "ServiceName" "mcp" "Ports" $mcpPorts "Release" $.Release "Values" $.Values -}}
{{- include "oneuptime.service" $mcpServiceArgs }}
---

View File

@@ -1799,45 +1799,6 @@
},
"additionalProperties": false
},
"mcp": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"replicaCount": {
"type": "integer"
},
"disableTelemetryCollection": {
"type": "boolean"
},
"disableAutoscaler": {
"type": "boolean"
},
"ports": {
"type": "object",
"properties": {
"http": {
"type": "integer"
}
},
"additionalProperties": false
},
"resources": {
"type": ["object", "null"]
},
"nodeSelector": {
"type": "object"
},
"podSecurityContext": {
"type": "object"
},
"containerSecurityContext": {
"type": "object"
}
},
"additionalProperties": false
},
"slackApp": {
"type": "object",
"properties": {

View File

@@ -732,18 +732,6 @@ isolatedVM:
podSecurityContext: {}
containerSecurityContext: {}
mcp:
enabled: true
replicaCount: 1
disableTelemetryCollection: false
disableAutoscaler: false
ports:
http: 3405
resources:
nodeSelector: {}
podSecurityContext: {}
containerSecurityContext: {}
# AI Agent Configuration
# Deploy this to run an AI Agent within your Kubernetes cluster
# Note: This is disabled by default. To enable, set enabled to true and provide the AI Agent credentials

View File

@@ -1579,6 +1579,18 @@ const HomeFeatureSet: FeatureSet = {
},
);
app.get("/legal/dpa", (_req: ExpressRequest, res: ExpressResponse) => {
res.render(`${ViewsPath}/legal.ejs`, {
footerCards: true,
support: false,
enableGoogleTagManager: IsBillingEnabled,
cta: true,
blackLogo: false,
section: "dpa",
requestDemoCta: false,
});
});
app.get("/legal/ccpa", (_req: ExpressRequest, res: ExpressResponse) => {
res.render(`${ViewsPath}/legal.ejs`, {
support: false,

221
Home/Views/dpa.ejs Normal file
View File

@@ -0,0 +1,221 @@
<header id="pagmt">
<h1>Data Processing Agreement</h1>
<p>Last updated: March 3, 2026</p>
</header>
<section>
<h2>1. Introduction</h2>
<p>
This Data Processing Agreement ("DPA") forms part of the Customer Terms of Service or other written agreement
between HackerBay, Inc. doing business as OneUptime ("Processor", "we", "us") and the entity agreeing to these
terms ("Controller", "Customer", "you") for the provision of OneUptime services (the "Agreement").
</p>
<p>
This DPA reflects the parties' commitment to abide by applicable Data Protection Laws concerning the processing
of Personal Data in connection with the Services provided under the Agreement.
</p>
<h2>2. Definitions</h2>
<p>In this DPA, the following terms have the meanings set out below. Capitalized terms not defined herein have the
meanings given to them in the Agreement.</p>
<ul>
<li><strong>"Controller"</strong> means the entity that determines the purposes and means of Processing Personal
Data, as defined in applicable Data Protection Laws.</li>
<li><strong>"Data Protection Laws"</strong> means all applicable legislation relating to data protection and
privacy, including the EU General Data Protection Regulation (GDPR) 2016/679, the California Consumer Privacy
Act (CCPA), and any applicable national implementing or supplementary legislation.</li>
<li><strong>"Data Subject"</strong> means an identified or identifiable natural person to whom Personal Data
relates.</li>
<li><strong>"Personal Data"</strong> means any information relating to a Data Subject that is processed by the
Processor on behalf of the Controller in connection with the Services.</li>
<li><strong>"Processing"</strong> (and "Process") means any operation performed on Personal Data, including
collection, recording, organization, structuring, storage, adaptation, alteration, retrieval, consultation,
use, disclosure, dissemination, restriction, erasure, or destruction.</li>
<li><strong>"Processor"</strong> means the entity that Processes Personal Data on behalf of the Controller, as
defined in applicable Data Protection Laws.</li>
<li><strong>"Security Incident"</strong> means any accidental or unlawful destruction, loss, alteration,
unauthorized disclosure of, or access to Personal Data.</li>
<li><strong>"Services"</strong> means the monitoring and incident management services provided by OneUptime to the
Customer under the Agreement.</li>
<li><strong>"Subprocessor"</strong> means any third party engaged by the Processor to Process Personal Data on
behalf of the Controller. A current list of Subprocessors is available at
<a href="/legal/subprocessors">/legal/subprocessors</a>.</li>
</ul>
<h2>3. Scope and Purpose of Processing</h2>
<h3>3.1 Scope</h3>
<p>
This DPA applies to the Processing of Personal Data by the Processor on behalf of the Controller in connection
with the provision of the Services under the Agreement.
</p>
<h3>3.2 Purpose</h3>
<p>
The Processor shall Process Personal Data only for the purposes of providing the Services as described in the
Agreement and as further documented in the Controller's instructions. The categories of Personal Data and Data
Subjects are determined by the Controller's use of the Services and typically include:
</p>
<ul>
<li><strong>Categories of Data Subjects:</strong> Customer's end users, employees, contractors, and other
individuals whose data is submitted to the Services.</li>
<li><strong>Types of Personal Data:</strong> Name, email address, IP address, user agent information, and any
other data submitted through the Services by the Controller.</li>
<li><strong>Processing Activities:</strong> Storage, analysis, monitoring, alerting, and incident management as
necessary to provide the Services.</li>
</ul>
<h2>4. Obligations of the Processor</h2>
<h3>4.1 Processing Instructions</h3>
<p>
The Processor shall Process Personal Data only on documented instructions from the Controller, including with
regard to transfers of Personal Data to a third country, unless required to do so by applicable law. In such a
case, the Processor shall inform the Controller of that legal requirement before Processing, unless prohibited
by law.
</p>
<h3>4.2 Confidentiality</h3>
<p>
The Processor shall ensure that persons authorized to Process Personal Data have committed themselves to
confidentiality or are under an appropriate statutory obligation of confidentiality.
</p>
<h3>4.3 Security Measures</h3>
<p>
The Processor shall implement appropriate technical and organizational measures to ensure a level of security
appropriate to the risk, including as appropriate:
</p>
<ul>
<li>Encryption of Personal Data in transit and at rest</li>
<li>Measures to ensure ongoing confidentiality, integrity, availability, and resilience of processing systems</li>
<li>The ability to restore the availability and access to Personal Data in a timely manner in the event of an
incident</li>
<li>Regular testing, assessing, and evaluating the effectiveness of technical and organizational measures</li>
</ul>
<h3>4.4 Subprocessing</h3>
<p>
The Processor shall not engage another processor (Subprocessor) without prior specific or general written
authorization of the Controller. In the case of general written authorization, the Processor shall inform the
Controller of any intended changes concerning the addition or replacement of Subprocessors, giving the Controller
the opportunity to object to such changes. A current list of Subprocessors is maintained at
<a href="/legal/subprocessors">/legal/subprocessors</a>.
</p>
<p>
Where the Processor engages a Subprocessor, the Processor shall impose on the Subprocessor the same data
protection obligations as set out in this DPA by way of a written contract, ensuring that the Subprocessor
provides sufficient guarantees to implement appropriate technical and organizational measures.
</p>
<h3>4.5 Assistance to the Controller</h3>
<p>
Taking into account the nature of the Processing, the Processor shall assist the Controller by appropriate
technical and organizational measures, insofar as possible, for the fulfilment of the Controller's obligation to
respond to requests from Data Subjects exercising their rights under Data Protection Laws.
</p>
<h2>5. Obligations of the Controller</h2>
<p>The Controller warrants and represents that:</p>
<ul>
<li>It has complied and will continue to comply with all applicable Data Protection Laws in respect of its use of
the Services and its Processing instructions to the Processor.</li>
<li>It has obtained all necessary consents or has another lawful basis for the transfer of Personal Data to the
Processor for Processing in accordance with this DPA.</li>
<li>It shall be responsible for the accuracy, quality, and legality of Personal Data and the means by which it
acquired the Personal Data.</li>
<li>It shall inform the Processor without undue delay if it becomes aware of any circumstances that could affect
the lawfulness of the Processing of Personal Data under this DPA.</li>
</ul>
<h2>6. Data Subject Rights</h2>
<p>
The Processor shall, to the extent legally permitted, promptly notify the Controller if the Processor receives a
request from a Data Subject to exercise any of the following rights with respect to their Personal Data:
</p>
<ul>
<li>Right of access</li>
<li>Right to rectification</li>
<li>Right to erasure ("right to be forgotten")</li>
<li>Right to restriction of processing</li>
<li>Right to data portability</li>
<li>Right to object to processing</li>
<li>Rights related to automated decision-making and profiling</li>
</ul>
<p>
The Processor shall not independently respond to such requests except on the documented instructions of the
Controller or as required by applicable law.
</p>
<h2>7. Security Incident Notification</h2>
<p>
The Processor shall notify the Controller without undue delay after becoming aware of a Security Incident
affecting Personal Data processed on behalf of the Controller. Such notification shall include:
</p>
<ul>
<li>A description of the nature of the Security Incident, including the categories and approximate number of
Data Subjects and Personal Data records affected</li>
<li>The name and contact details of the Processor's data protection contact</li>
<li>A description of the likely consequences of the Security Incident</li>
<li>A description of the measures taken or proposed to be taken to address the Security Incident, including
measures to mitigate its possible adverse effects</li>
</ul>
<p>
The Processor shall cooperate with the Controller and take reasonable commercial steps to assist in the
investigation, mitigation, and remediation of each Security Incident.
</p>
<h2>8. International Data Transfers</h2>
<p>
The Processor shall not transfer Personal Data to a country outside the European Economic Area (EEA) unless
appropriate safeguards are in place as required by applicable Data Protection Laws, such as:
</p>
<ul>
<li>Standard Contractual Clauses (SCCs) approved by the European Commission</li>
<li>Binding Corporate Rules</li>
<li>An adequacy decision by the European Commission for the recipient country</li>
<li>The EU-U.S. Data Privacy Framework, where applicable</li>
</ul>
<p>
For information about where Customer Data is stored, please refer to our
<a href="/legal/data-residency">Data Residency</a> page.
</p>
<h2>9. Duration and Termination</h2>
<p>
This DPA shall remain in effect for the duration of the Agreement. Upon termination of the Agreement, the
Processor shall, at the choice of the Controller, delete or return all Personal Data to the Controller and delete
existing copies, unless applicable law requires storage of the Personal Data. The Processor shall certify in
writing that it has complied with this provision upon the Controller's request.
</p>
<h2>10. Audit Rights</h2>
<p>
The Processor shall make available to the Controller all information necessary to demonstrate compliance with the
obligations laid down in this DPA and applicable Data Protection Laws, and shall allow for and contribute to
audits, including inspections, conducted by the Controller or another auditor mandated by the Controller.
</p>
<p>
The Controller shall provide reasonable prior written notice of any audit request. Audits shall be conducted
during normal business hours and shall not unreasonably interfere with the Processor's business operations. The
Controller shall bear the costs of any such audit unless the audit reveals a material breach of this DPA by the
Processor.
</p>
<h2>11. Liability</h2>
<p>
Each party's liability arising out of or related to this DPA is subject to the limitations of liability set forth
in the Agreement. In no event shall either party's aggregate liability for claims arising out of or related to
this DPA exceed the limitations set forth in the Agreement.
</p>
<h2>12. Contact Information</h2>
<p>
For questions or concerns about this DPA or our data processing practices, please contact us:
</p>
<ul>
<li><strong>Entity:</strong> HackerBay, Inc. (doing business as OneUptime)</li>
<li><strong>Email:</strong> <a href="mailto:legal@oneuptime.com">legal@oneuptime.com</a></li>
<li><strong>Address:</strong> 325 N Wells St, Chicago, IL 60654, United States</li>
</ul>
<p>
For more information about our privacy practices, please see our
<a href="/legal/privacy">Privacy Policy</a>. For details on GDPR compliance, visit our
<a href="/legal/gdpr">GDPR</a> page.
</p>
</section>

View File

@@ -310,6 +310,7 @@
<option value="/legal/hipaa" <%= section === 'hipaa' ? 'selected' : '' %>>HIPAA</option>
<option value="/legal/data-residency" <%= section === 'data-residency' ? 'selected' : '' %>>Data Residency</option>
<option value="/legal/subprocessors" <%= section === 'subprocessors' ? 'selected' : '' %>>Subprocessors</option>
<option value="/legal/dpa" <%= section === 'dpa' ? 'selected' : '' %>>Data Processing Agreement</option>
</optgroup>
<optgroup label="Security Certifications">
<option value="/legal/iso-27001" <%= section === 'iso-27001' ? 'selected' : '' %>>ISO/IEC 27001</option>
@@ -395,6 +396,11 @@
Subprocessors
</a>
</li>
<li>
<a href="/legal/dpa" class="toc-link flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors <%= section === 'dpa' ? 'nav-item-active bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' %>">
Data Processing Agreement
</a>
</li>
</ul>
</div>
@@ -563,6 +569,10 @@
<%- include('subprocessors') -%>
<% } %>
<% if(section === "dpa") { %>
<%- include('dpa') -%>
<% } %>
<% if(section === "vpat") { %>
<%- include('vpat') -%>
<% } %>

View File

@@ -56,10 +56,6 @@ upstream opentelemetry-collector-grpc {
server ${SERVER_OTEL_COLLECTOR_HOSTNAME}:4317;
}
upstream mcp {
server ${SERVER_MCP_HOSTNAME}:${MCP_PORT} weight=10 max_fails=3 fail_timeout=30s;
}
# Status Pages
server {
@@ -960,6 +956,6 @@ ${PROVISION_SSL_CERTIFICATE_KEY_DIRECTIVE}
proxy_send_timeout 86400s;
chunked_transfer_encoding on;
proxy_pass http://mcp;
proxy_pass http://app;
}
}

View File

@@ -104,7 +104,6 @@ SERVER_ADMIN_DASHBOARD_HOSTNAME=admin-dashboard
SERVER_OTEL_COLLECTOR_HOSTNAME=otel-collector
SERVER_WORKER_HOSTNAME=worker
SERVER_DOCS_HOSTNAME=docs
SERVER_MCP_HOSTNAME=mcp
#Ports. Usually they don't need to change.
@@ -121,7 +120,6 @@ HOME_PORT=1444
WORKER_PORT=1445
WORKFLOW_PORT=3099
DOCS_PORT=1447
MCP_PORT=3405
# Plans
# This is in the format of PlanName,PlanIdFromBillingProvider,MonthlySubscriptionPlanAmountInUSD,YearlySubscriptionPlanAmountInUSD,Order,TrialPeriodInDays
@@ -317,7 +315,6 @@ DISABLE_TELEMETRY_FOR_ISOLATED_VM=true
DISABLE_TELEMETRY_FOR_INGRESS=true
DISABLE_TELEMETRY_FOR_WORKER=true
DISABLE_TELEMETRY_FOR_MCP=true
DISABLE_TELEMETRY_FOR_AI_AGENT=true

View File

@@ -42,7 +42,6 @@ x-common-variables: &common-variables
SERVER_HOME_HOSTNAME: home
SERVER_WORKFLOW_HOSTNAME: workflow
SERVER_DOCS_HOSTNAME: docs
SERVER_MCP_HOSTNAME: mcp
#Ports. Usually they don't need to change.
APP_PORT: ${APP_PORT}
@@ -57,7 +56,6 @@ x-common-variables: &common-variables
WORKER_PORT: ${WORKER_PORT}
WORKFLOW_PORT: ${WORKFLOW_PORT}
DOCS_PORT: ${DOCS_PORT}
MCP_PORT: ${MCP_PORT}
OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT: ${OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT}
OPENTELEMETRY_EXPORTER_OTLP_HEADERS: ${OPENTELEMETRY_EXPORTER_OTLP_HEADERS}
@@ -502,19 +500,6 @@ services:
options:
max-size: "1000m"
mcp:
networks:
- oneuptime
restart: always
environment:
<<: *common-runtime-variables
PORT: ${MCP_PORT}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY_FOR_MCP}
logging:
driver: "local"
options:
max-size: "1000m"
e2e:
restart: "no"
network_mode: host # This is needed to access the host network,

View File

@@ -377,26 +377,6 @@ services:
context: .
dockerfile: ./Telemetry/Dockerfile
mcp:
volumes:
- ./MCP:/usr/src/app:cached
# Use node modules of the container and not host system.
# https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder
- /usr/src/app/node_modules/
- ./Common:/usr/src/Common:cached
- /usr/src/Common/node_modules/
ports:
- '9945:9229' # Debugging port.
extends:
file: ./docker-compose.base.yml
service: mcp
depends_on:
<<: *common-depends-on
build:
network: host
context: .
dockerfile: ./MCP/Dockerfile
# Fluentd. Required only for development. In production its the responsibility of the customer to run fluentd and pipe logs to OneUptime.
# We run this container just for development, to see if logs are piped.

View File

@@ -129,14 +129,6 @@ services:
file: ./docker-compose.base.yml
service: isolated-vm
mcp:
image: oneuptime/mcp:${APP_TAG}
extends:
file: ./docker-compose.base.yml
service: mcp
depends_on:
<<: *common-depends-on
ingress:
image: oneuptime/nginx:${APP_TAG}
extends: