mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
17
.github/workflows/compile.yml
vendored
17
.github/workflows/compile.yml
vendored
@@ -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:
|
||||
|
||||
72
.github/workflows/release.yml
vendored
72
.github/workflows/release.yml
vendored
@@ -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:
|
||||
|
||||
72
.github/workflows/test-release.yaml
vendored
72
.github/workflows/test-release.yaml
vendored
@@ -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:
|
||||
|
||||
21
.github/workflows/test.mcp.yaml
vendored
21
.github/workflows/test.mcp.yaml
vendored
@@ -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
|
||||
30
App/FeatureSet/MCP/Config/ServerConfig.ts
Normal file
30
App/FeatureSet/MCP/Config/ServerConfig.ts
Normal 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;
|
||||
232
App/FeatureSet/MCP/Handlers/RouteHandler.ts
Normal file
232
App/FeatureSet/MCP/Handlers/RouteHandler.ts
Normal 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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
376
App/FeatureSet/MCP/Handlers/ToolHandler.ts
Normal file
376
App/FeatureSet/MCP/Handlers/ToolHandler.ts
Normal 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);
|
||||
}
|
||||
51
App/FeatureSet/MCP/Index.ts
Normal file
51
App/FeatureSet/MCP/Index.ts
Normal 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;
|
||||
66
App/FeatureSet/MCP/Server/MCPServer.ts
Normal file
66
App/FeatureSet/MCP/Server/MCPServer.ts
Normal 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 };
|
||||
113
App/FeatureSet/MCP/Server/SessionManager.ts
Normal file
113
App/FeatureSet/MCP/Server/SessionManager.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
378
App/FeatureSet/MCP/Services/OneUptimeApiService.ts
Normal file
378
App/FeatureSet/MCP/Services/OneUptimeApiService.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
304
App/FeatureSet/MCP/Services/SelectFieldGenerator.ts
Normal file
304
App/FeatureSet/MCP/Services/SelectFieldGenerator.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
392
App/FeatureSet/MCP/Tools/HelperTools.ts
Normal file
392
App/FeatureSet/MCP/Tools/HelperTools.ts
Normal 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);
|
||||
}
|
||||
415
App/FeatureSet/MCP/Tools/PublicStatusPageTools.ts
Normal file
415
App/FeatureSet/MCP/Tools/PublicStatusPageTools.ts
Normal 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;
|
||||
}
|
||||
257
App/FeatureSet/MCP/Tools/SchemaConverter.ts
Normal file
257
App/FeatureSet/MCP/Tools/SchemaConverter.ts
Normal 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, "")
|
||||
);
|
||||
}
|
||||
541
App/FeatureSet/MCP/Tools/ToolGenerator.ts
Normal file
541
App/FeatureSet/MCP/Tools/ToolGenerator.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
68
App/FeatureSet/MCP/Types/McpTypes.ts
Normal file
68
App/FeatureSet/MCP/Types/McpTypes.ts
Normal 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;
|
||||
}
|
||||
6
App/FeatureSet/MCP/Types/ModelType.ts
Normal file
6
App/FeatureSet/MCP/Types/ModelType.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum ModelType {
|
||||
Database = "database",
|
||||
Analytics = "analytics",
|
||||
}
|
||||
|
||||
export default ModelType;
|
||||
10
App/FeatureSet/MCP/Types/OneUptimeOperation.ts
Normal file
10
App/FeatureSet/MCP/Types/OneUptimeOperation.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum OneUptimeOperation {
|
||||
Create = "create",
|
||||
Read = "read",
|
||||
List = "list",
|
||||
Update = "update",
|
||||
Delete = "delete",
|
||||
Count = "count",
|
||||
}
|
||||
|
||||
export default OneUptimeOperation;
|
||||
84
App/FeatureSet/MCP/Utils/MCPLogger.ts
Normal file
84
App/FeatureSet/MCP/Utils/MCPLogger.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
---
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
221
Home/Views/dpa.ejs
Normal 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>
|
||||
@@ -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') -%>
|
||||
<% } %>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user