mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Refactor SelectFieldGenerator and SchemaConverter for improved readability and maintainability
- Updated formatting and indentation for consistency in SelectFieldGenerator.ts and SchemaConverter.ts. - Enhanced logging messages for better debugging in generateAllFieldsSelect and findModelClass functions. - Simplified error handling and fallback mechanisms in generateAllFieldsSelect. - Improved type definitions and structure in Zod schema conversion functions. - Added tests for server initialization and tool management to ensure proper functionality and error handling.
This commit is contained in:
@@ -17,7 +17,7 @@ export const ROUTE_PREFIXES: string[] = [`/${APP_NAME}`, "/"];
|
||||
|
||||
// API URL configuration
|
||||
export function getApiUrl(): string {
|
||||
return Host ? `${HttpProtocol}${Host}` : "https://oneuptime.com";
|
||||
return Host ? `${HttpProtocol}${Host}` : "https://oneuptime.com";
|
||||
}
|
||||
|
||||
// Session header name
|
||||
|
||||
@@ -6,224 +6,237 @@
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
ExpressApplication,
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
ExpressJson,
|
||||
ExpressApplication,
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
NextFunction,
|
||||
ExpressJson,
|
||||
} from "Common/Server/Utils/Express";
|
||||
import { getMCPServer } from "../Server/MCPServer";
|
||||
import SessionManager, { SessionData } from "../Server/SessionManager";
|
||||
import { McpToolInfo } from "../Types/McpTypes";
|
||||
import {
|
||||
ROUTE_PREFIXES,
|
||||
SESSION_HEADER,
|
||||
API_KEY_HEADERS,
|
||||
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
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
});
|
||||
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(", ")}`);
|
||||
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[]
|
||||
app: ExpressApplication,
|
||||
prefix: string,
|
||||
tools: McpToolInfo[],
|
||||
): void {
|
||||
const mcpEndpoint: string = prefix === "/" ? "/mcp" : `${prefix}/mcp`;
|
||||
const mcpHandler: McpHandlerFunction = createMCPHandler();
|
||||
const mcpEndpoint: string = prefix === "/" ? "/mcp" : `${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);
|
||||
// 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();
|
||||
// OPTIONS handler for CORS preflight requests
|
||||
app.options(mcpEndpoint, (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.status(200).end();
|
||||
});
|
||||
|
||||
// Handle root "/" when nginx strips the /mcp/ prefix (for "/" prefix only)
|
||||
if (prefix === "/") {
|
||||
app.get("/", mcpHandler);
|
||||
app.post("/", ExpressJson(), mcpHandler);
|
||||
app.delete("/", mcpHandler);
|
||||
app.options("/", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.status(200).end();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle root "/" when nginx strips the /mcp/ prefix (for "/" prefix only)
|
||||
if (prefix === "/") {
|
||||
app.get("/", mcpHandler);
|
||||
app.post("/", ExpressJson(), mcpHandler);
|
||||
app.delete("/", mcpHandler);
|
||||
app.options("/", (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.status(200).end();
|
||||
});
|
||||
}
|
||||
// List tools endpoint (REST API)
|
||||
setupToolsEndpoint(app, prefix, tools);
|
||||
|
||||
// List tools endpoint (REST API)
|
||||
setupToolsEndpoint(app, prefix, tools);
|
||||
|
||||
// Health check endpoint
|
||||
setupHealthEndpoint(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);
|
||||
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 || "");
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
// 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
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
sessionId: string,
|
||||
apiKey: string,
|
||||
): Promise<void> {
|
||||
const sessionData: SessionData | undefined = SessionManager.getSession(sessionId);
|
||||
const sessionData: SessionData | undefined =
|
||||
SessionManager.getSession(sessionId);
|
||||
|
||||
if (!sessionData) {
|
||||
return;
|
||||
}
|
||||
if (!sessionData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update API key in case it changed
|
||||
sessionData.apiKey = apiKey;
|
||||
await sessionData.transport.handleRequest(req, res, req.body);
|
||||
// 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
|
||||
req: ExpressRequest,
|
||||
res: ExpressResponse,
|
||||
apiKey: string,
|
||||
): Promise<void> {
|
||||
const mcpServer = getMCPServer();
|
||||
const mcpServer = getMCPServer();
|
||||
|
||||
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: (): string => 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}`);
|
||||
},
|
||||
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 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}`);
|
||||
};
|
||||
// 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]);
|
||||
// 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);
|
||||
// Handle the request
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the tools listing endpoint
|
||||
*/
|
||||
function setupToolsEndpoint(
|
||||
app: ExpressApplication,
|
||||
prefix: string,
|
||||
tools: McpToolInfo[]
|
||||
app: ExpressApplication,
|
||||
prefix: string,
|
||||
tools: McpToolInfo[],
|
||||
): void {
|
||||
const endpoint: string = `${prefix === "/" ? "" : prefix}/tools`;
|
||||
const endpoint: string = `${prefix === "/" ? "" : prefix}/tools`;
|
||||
|
||||
app.get(endpoint, (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
const toolsList: Array<{ name: string; description: string }> = tools.map(
|
||||
(tool: McpToolInfo) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
})
|
||||
);
|
||||
res.json({ tools: toolsList, count: toolsList.length });
|
||||
});
|
||||
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[]
|
||||
app: ExpressApplication,
|
||||
prefix: string,
|
||||
tools: McpToolInfo[],
|
||||
): void {
|
||||
const endpoint: string = `${prefix === "/" ? "" : prefix}/health`;
|
||||
const endpoint: string = `${prefix === "/" ? "" : prefix}/health`;
|
||||
|
||||
app.get(endpoint, (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.json({
|
||||
status: "healthy",
|
||||
service: "oneuptime-mcp",
|
||||
tools: tools.length,
|
||||
activeSessions: SessionManager.getSessionCount(),
|
||||
});
|
||||
app.get(endpoint, (_req: ExpressRequest, res: ExpressResponse) => {
|
||||
res.json({
|
||||
status: "healthy",
|
||||
service: "oneuptime-mcp",
|
||||
tools: tools.length,
|
||||
activeSessions: SessionManager.getSessionCount(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
122
MCP/Index.ts
122
MCP/Index.ts
@@ -17,7 +17,9 @@ 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 OneUptimeApiService, {
|
||||
OneUptimeApiConfig,
|
||||
} from "./Services/OneUptimeApiService";
|
||||
import { McpToolInfo } from "./Types/McpTypes";
|
||||
|
||||
const app: ExpressApplication = Express.getExpressApp();
|
||||
@@ -26,94 +28,98 @@ const app: ExpressApplication = Express.getExpressApp();
|
||||
* Initialize OneUptime API Service
|
||||
*/
|
||||
function initializeApiService(): void {
|
||||
const apiUrl: string = getApiUrl();
|
||||
const apiUrl: string = getApiUrl();
|
||||
|
||||
const config: OneUptimeApiConfig = {
|
||||
url: apiUrl,
|
||||
};
|
||||
const config: OneUptimeApiConfig = {
|
||||
url: apiUrl,
|
||||
};
|
||||
|
||||
OneUptimeApiService.initialize(config);
|
||||
logger.info(
|
||||
`OneUptime API Service initialized with: ${apiUrl} (API keys provided per-request via x-api-key header)`
|
||||
);
|
||||
OneUptimeApiService.initialize(config);
|
||||
logger.info(
|
||||
`OneUptime API Service initialized with: ${apiUrl} (API keys provided per-request via x-api-key header)`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate MCP tools for all models
|
||||
*/
|
||||
function generateTools(): McpToolInfo[] {
|
||||
try {
|
||||
const tools: McpToolInfo[] = generateAllTools();
|
||||
logger.info(`Generated ${tools.length} OneUptime MCP tools`);
|
||||
return tools;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate tools: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
const tools: McpToolInfo[] = generateAllTools();
|
||||
logger.info(`Generated ${tools.length} OneUptime MCP tools`);
|
||||
return tools;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate tools: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple status check for MCP (no database connections)
|
||||
*/
|
||||
const statusCheck: PromiseVoidFunction = async (): Promise<void> => {
|
||||
// MCP server doesn't connect to databases directly
|
||||
// Just verify the server is running
|
||||
return Promise.resolve();
|
||||
/*
|
||||
* MCP server doesn't connect to databases directly
|
||||
* Just verify the server is running
|
||||
*/
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
/**
|
||||
* Main initialization function
|
||||
*/
|
||||
const init: PromiseVoidFunction = async (): Promise<void> => {
|
||||
try {
|
||||
// Initialize telemetry
|
||||
Telemetry.init({
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
try {
|
||||
// Initialize telemetry
|
||||
Telemetry.init({
|
||||
serviceName: APP_NAME,
|
||||
});
|
||||
|
||||
// Initialize the app with service name and status checks
|
||||
await App.init({
|
||||
appName: APP_NAME,
|
||||
statusOptions: {
|
||||
liveCheck: statusCheck,
|
||||
readyCheck: statusCheck,
|
||||
},
|
||||
});
|
||||
// Initialize the app with service name and status checks
|
||||
await App.init({
|
||||
appName: APP_NAME,
|
||||
statusOptions: {
|
||||
liveCheck: statusCheck,
|
||||
readyCheck: statusCheck,
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize services
|
||||
initializeApiService();
|
||||
// Initialize services
|
||||
initializeApiService();
|
||||
|
||||
// Initialize MCP server
|
||||
initializeMCPServer();
|
||||
// Initialize MCP server
|
||||
initializeMCPServer();
|
||||
|
||||
// Generate tools
|
||||
const tools: McpToolInfo[] = generateTools();
|
||||
// Generate tools
|
||||
const tools: McpToolInfo[] = generateTools();
|
||||
|
||||
// Register tool handlers
|
||||
registerToolHandlers(getMCPServer(), tools);
|
||||
// Register tool handlers
|
||||
registerToolHandlers(getMCPServer(), tools);
|
||||
|
||||
// Setup MCP-specific routes
|
||||
setupMCPRoutes(app, tools);
|
||||
// Setup MCP-specific routes
|
||||
setupMCPRoutes(app, tools);
|
||||
|
||||
// Add default routes to the app
|
||||
await App.addDefaultRoutes();
|
||||
// Add default routes to the app
|
||||
await App.addDefaultRoutes();
|
||||
|
||||
logger.info(`OneUptime MCP Server started successfully`);
|
||||
logger.info(`Available tools: ${tools.length} total`);
|
||||
logger.info(`OneUptime MCP Server started successfully`);
|
||||
logger.info(`Available tools: ${tools.length} total`);
|
||||
|
||||
// Log some example tools
|
||||
const exampleTools: string[] = tools.slice(0, 5).map((t: McpToolInfo) => t.name);
|
||||
logger.info(`Example tools: ${exampleTools.join(", ")}`);
|
||||
} catch (err) {
|
||||
logger.error("MCP Server Init Failed:");
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
// Log some example tools
|
||||
const exampleTools: string[] = tools.slice(0, 5).map((t: McpToolInfo) => {
|
||||
return t.name;
|
||||
});
|
||||
logger.info(`Example tools: ${exampleTools.join(", ")}`);
|
||||
} catch (err) {
|
||||
logger.error("MCP Server Init Failed:");
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Start the server
|
||||
init().catch((err: Error) => {
|
||||
logger.error(err);
|
||||
logger.error("Exiting node process");
|
||||
process.exit(1);
|
||||
logger.error(err);
|
||||
logger.error("Exiting node process");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -14,24 +14,26 @@ 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}`);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,24 +41,26 @@ export function initializeMCPServer(): McpServer {
|
||||
* @throws Error if server not initialized
|
||||
*/
|
||||
export function getMCPServer(): McpServer {
|
||||
if (!mcpServerInstance) {
|
||||
throw new Error("MCP Server not initialized. Call initializeMCPServer() first.");
|
||||
}
|
||||
return mcpServerInstance;
|
||||
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;
|
||||
return mcpServerInstance !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset MCP server (useful for testing)
|
||||
*/
|
||||
export function resetMCPServer(): void {
|
||||
mcpServerInstance = null;
|
||||
mcpServerInstance = null;
|
||||
}
|
||||
|
||||
export { McpServer };
|
||||
|
||||
@@ -8,103 +8,106 @@ import logger from "Common/Server/Utils/Logger";
|
||||
|
||||
// Session data interface
|
||||
export interface SessionData {
|
||||
transport: StreamableHTTPServerTransport;
|
||||
apiKey: string;
|
||||
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 = "";
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
/**
|
||||
* 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}`);
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
/**
|
||||
* 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 = "";
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
/**
|
||||
* Clear all sessions (useful for cleanup)
|
||||
*/
|
||||
public static clearAllSessions(): void {
|
||||
this.sessions.clear();
|
||||
this.currentSessionApiKey = "";
|
||||
logger.info("All MCP sessions cleared");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,330 +19,335 @@ import Protocol from "Common/Types/API/Protocol";
|
||||
import Hostname from "Common/Types/API/Hostname";
|
||||
|
||||
export interface OneUptimeApiConfig {
|
||||
url: string;
|
||||
apiKey?: string;
|
||||
url: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export default class OneUptimeApiService {
|
||||
private static api: API;
|
||||
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;
|
||||
/**
|
||||
* 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}`);
|
||||
this.api = new API(protocol, hostname, new Route("/"));
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid URL format: ${config.url}. Error: ${error}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
MCPLogger.info(`OneUptime API Service initialized with: ${config.url}`);
|
||||
}
|
||||
|
||||
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
|
||||
/**
|
||||
* 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 = 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,
|
||||
);
|
||||
|
||||
MCPLogger.info(
|
||||
`Executing ${operation} operation for ${tableName} at ${route.toString()}`
|
||||
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}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = 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;
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`);
|
||||
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;
|
||||
}
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw new Error(
|
||||
`API request failed: ${response.statusCode} - ${response.message}`
|
||||
);
|
||||
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 = ["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 = ["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 = ["id", "query", "select", "skip", "limit", "sort"];
|
||||
|
||||
switch (operation) {
|
||||
case OneUptimeOperation.Create: {
|
||||
const createDataFields = 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",
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
break;
|
||||
}
|
||||
case OneUptimeOperation.Read:
|
||||
case OneUptimeOperation.Delete:
|
||||
if (!args.id) {
|
||||
throw new Error(`ID is required for ${operation} operation`);
|
||||
}
|
||||
|
||||
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;
|
||||
break;
|
||||
case OneUptimeOperation.Update: {
|
||||
if (!args.id) {
|
||||
throw new Error(`ID is required for ${operation} operation`);
|
||||
}
|
||||
}
|
||||
|
||||
private static buildCreateData(args: OneUptimeToolCallArgs): JSONObject {
|
||||
const createData: JSONObject = {};
|
||||
const reservedFields = ["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 = ["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 = ["id", "query", "select", "skip", "limit", "sort"];
|
||||
|
||||
switch (operation) {
|
||||
case OneUptimeOperation.Create: {
|
||||
const createDataFields = Object.keys(args).filter(
|
||||
(key: string) => !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 = Object.keys(args).filter(
|
||||
(key: string) => !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}`);
|
||||
const updateDataFields = 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,18 @@ type ModelConstructor<T> = new () => T;
|
||||
|
||||
// Type for model class with table name
|
||||
interface ModelWithTableName {
|
||||
tableName: string;
|
||||
getColumnAccessControlForAllColumns?: () => Record<string, ColumnAccessControl>;
|
||||
tableName: string;
|
||||
getColumnAccessControlForAllColumns?: () => Record<
|
||||
string,
|
||||
ColumnAccessControl
|
||||
>;
|
||||
}
|
||||
|
||||
// Type for column access control
|
||||
interface ColumnAccessControl {
|
||||
read?: Permission[];
|
||||
create?: Permission[];
|
||||
update?: Permission[];
|
||||
read?: Permission[];
|
||||
create?: Permission[];
|
||||
update?: Permission[];
|
||||
}
|
||||
|
||||
// Type for table columns
|
||||
@@ -36,253 +39,263 @@ type TableColumns = Record<string, unknown>;
|
||||
|
||||
// Type for Zod schema shape
|
||||
interface ZodSchemaWithShape {
|
||||
_def?: {
|
||||
shape?: Record<string, unknown> | (() => Record<string, unknown>);
|
||||
};
|
||||
_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
|
||||
tableName: string,
|
||||
modelType: ModelType,
|
||||
): JSONObject {
|
||||
MCPLogger.info(
|
||||
`Generating select for tableName: ${tableName}, modelType: ${modelType}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const ModelClass = findModelClass(tableName, modelType);
|
||||
|
||||
if (!ModelClass) {
|
||||
MCPLogger.warn(
|
||||
`Model class not found for ${tableName}, using empty select`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
MCPLogger.info(
|
||||
`Generating select for tableName: ${tableName}, modelType: ${modelType}`
|
||||
`Found ModelClass: ${(ModelClass as { name: string }).name} for tableName: ${tableName}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const ModelClass = findModelClass(tableName, modelType);
|
||||
// Try to get raw table columns first (most reliable approach)
|
||||
const selectFromColumns: JSONObject | null = generateSelectFromTableColumns(
|
||||
ModelClass as ModelConstructor<BaseModel>,
|
||||
tableName,
|
||||
);
|
||||
|
||||
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();
|
||||
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
|
||||
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.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
|
||||
);
|
||||
}
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate select object from table columns
|
||||
*/
|
||||
function generateSelectFromTableColumns(
|
||||
ModelClass: ModelConstructor<BaseModel>,
|
||||
tableName: string
|
||||
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);
|
||||
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(", ")}`
|
||||
);
|
||||
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;
|
||||
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>
|
||||
columnName: string,
|
||||
accessControlForColumns: Record<string, ColumnAccessControl>,
|
||||
): boolean {
|
||||
const accessControl: ColumnAccessControl | undefined =
|
||||
accessControlForColumns[columnName];
|
||||
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
|
||||
))
|
||||
);
|
||||
/*
|
||||
* 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
|
||||
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;
|
||||
}
|
||||
}
|
||||
let selectSchema: ZodSchemaWithShape;
|
||||
|
||||
if (modelType === ModelType.Database) {
|
||||
MCPLogger.info(
|
||||
`Generated select for ${tableName} with ${Object.keys(selectObject).length} fields`
|
||||
`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 ? "..." : ""}`,
|
||||
);
|
||||
|
||||
// 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();
|
||||
for (const fieldName of fieldNames) {
|
||||
selectObject[fieldName] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return selectObject;
|
||||
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,
|
||||
};
|
||||
return {
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,224 +9,232 @@ import { AnalyticsModelSchemaType } from "Common/Utils/Schema/AnalyticsModelSche
|
||||
|
||||
// Type for Zod field definition
|
||||
interface ZodFieldDef {
|
||||
typeName?: string;
|
||||
innerType?: ZodField;
|
||||
description?: string;
|
||||
openapi?: {
|
||||
metadata?: OpenApiMetadata;
|
||||
};
|
||||
typeName?: string;
|
||||
innerType?: ZodField;
|
||||
description?: string;
|
||||
openapi?: {
|
||||
metadata?: OpenApiMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
// Type for Zod field
|
||||
interface ZodField {
|
||||
_def?: ZodFieldDef;
|
||||
_def?: ZodFieldDef;
|
||||
}
|
||||
|
||||
// Type for OpenAPI metadata
|
||||
interface OpenApiMetadata {
|
||||
type?: string;
|
||||
description?: string;
|
||||
example?: unknown;
|
||||
format?: string;
|
||||
default?: unknown;
|
||||
items?: JSONSchemaProperty;
|
||||
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>;
|
||||
};
|
||||
_def?: {
|
||||
shape?: () => Record<string, ZodField>;
|
||||
};
|
||||
}
|
||||
|
||||
// Result type for schema conversion
|
||||
export interface ZodToJsonSchemaResult {
|
||||
type: string;
|
||||
properties: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties: boolean;
|
||||
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
|
||||
zodSchema: ModelSchemaType | AnalyticsModelSchemaType,
|
||||
): ZodToJsonSchemaResult {
|
||||
try {
|
||||
const schemaWithShape: ZodSchemaWithShape = zodSchema as unknown as ZodSchemaWithShape;
|
||||
const shapeFunction: (() => Record<string, ZodField>) | undefined =
|
||||
schemaWithShape._def?.shape;
|
||||
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();
|
||||
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
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
// 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);
|
||||
// Clean up description
|
||||
const rawDescription: string =
|
||||
zodField._def?.description ||
|
||||
openApiMetadata?.description ||
|
||||
`${key} field`;
|
||||
const cleanDescription: string = cleanFieldDescription(rawDescription);
|
||||
|
||||
let property: JSONSchemaProperty;
|
||||
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,
|
||||
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
|
||||
metadata: OpenApiMetadata,
|
||||
key: string,
|
||||
description: string,
|
||||
): JSONSchemaProperty {
|
||||
const property: JSONSchemaProperty = {
|
||||
type: metadata.type || "string",
|
||||
description,
|
||||
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`,
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const punctuation: string[] = [".", "!", "?"];
|
||||
const lastChar: string = text.charAt(text.length - 1);
|
||||
const punctuation: string[] = [".", "!", "?"];
|
||||
const lastChar: string = text.charAt(text.length - 1);
|
||||
|
||||
if (punctuation.includes(lastChar)) {
|
||||
return text;
|
||||
}
|
||||
if (punctuation.includes(lastChar)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text + ".";
|
||||
return text + ".";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty schema result
|
||||
*/
|
||||
function createEmptySchema(): ZodToJsonSchemaResult {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
};
|
||||
return {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,16 +242,16 @@ function createEmptySchema(): ZodToJsonSchemaResult {
|
||||
* 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, "")
|
||||
);
|
||||
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, "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,128 +11,130 @@ jest.mock("../Tools/ToolGenerator");
|
||||
jest.mock("../Utils/MCPLogger");
|
||||
|
||||
describe("OneUptime MCP Server", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env["ONEUPTIME_API_KEY"] = "test-api-key";
|
||||
process.env["ONEUPTIME_URL"] = "https://test.oneuptime.com";
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env["ONEUPTIME_API_KEY"] = "test-api-key";
|
||||
process.env["ONEUPTIME_URL"] = "https://test.oneuptime.com";
|
||||
});
|
||||
|
||||
describe("Server Initialization", () => {
|
||||
it("should initialize with proper configuration", () => {
|
||||
const mockTools: McpToolInfo[] = [
|
||||
{
|
||||
name: "create_project",
|
||||
description: "Create a new project",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
modelName: "Project",
|
||||
operation: OneUptimeOperation.Create,
|
||||
modelType: ModelType.Database,
|
||||
singularName: "project",
|
||||
pluralName: "projects",
|
||||
tableName: "Project",
|
||||
apiPath: "/api/project",
|
||||
},
|
||||
];
|
||||
|
||||
(ToolGenerator.generateAllTools as jest.Mock).mockReturnValue(mockTools);
|
||||
(OneUptimeApiService.initialize as jest.Mock).mockImplementation(
|
||||
() => {},
|
||||
);
|
||||
|
||||
// Call the mocked functions to simulate server initialization
|
||||
ToolGenerator.generateAllTools();
|
||||
OneUptimeApiService.initialize({
|
||||
url: "https://test.oneuptime.com",
|
||||
apiKey: "test-api-key",
|
||||
});
|
||||
|
||||
// Test that the functions were called
|
||||
expect(ToolGenerator.generateAllTools).toHaveBeenCalled();
|
||||
expect(OneUptimeApiService.initialize).toHaveBeenCalledWith({
|
||||
url: "https://test.oneuptime.com",
|
||||
apiKey: "test-api-key",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Server Initialization", () => {
|
||||
it("should initialize with proper configuration", () => {
|
||||
const mockTools: McpToolInfo[] = [
|
||||
{
|
||||
name: "create_project",
|
||||
description: "Create a new project",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
modelName: "Project",
|
||||
operation: OneUptimeOperation.Create,
|
||||
modelType: ModelType.Database,
|
||||
singularName: "project",
|
||||
pluralName: "projects",
|
||||
tableName: "Project",
|
||||
apiPath: "/api/project",
|
||||
},
|
||||
];
|
||||
it("should throw error when API key is missing", () => {
|
||||
// Mock the service to throw error for missing API key
|
||||
(OneUptimeApiService.initialize as jest.Mock).mockImplementation(
|
||||
(config: unknown) => {
|
||||
const typedConfig = config as { url: string; apiKey: string };
|
||||
if (!typedConfig.apiKey) {
|
||||
throw new Error("OneUptime API key is required");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
(ToolGenerator.generateAllTools as jest.Mock).mockReturnValue(mockTools);
|
||||
(OneUptimeApiService.initialize as jest.Mock).mockImplementation(() => {});
|
||||
|
||||
// Call the mocked functions to simulate server initialization
|
||||
ToolGenerator.generateAllTools();
|
||||
OneUptimeApiService.initialize({
|
||||
url: "https://test.oneuptime.com",
|
||||
apiKey: "test-api-key",
|
||||
});
|
||||
|
||||
// Test that the functions were called
|
||||
expect(ToolGenerator.generateAllTools).toHaveBeenCalled();
|
||||
expect(OneUptimeApiService.initialize).toHaveBeenCalledWith({
|
||||
url: "https://test.oneuptime.com",
|
||||
apiKey: "test-api-key",
|
||||
});
|
||||
expect(() => {
|
||||
OneUptimeApiService.initialize({
|
||||
url: "https://test.oneuptime.com",
|
||||
apiKey: "",
|
||||
});
|
||||
}).toThrow("OneUptime API key is required");
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error when API key is missing", () => {
|
||||
// Mock the service to throw error for missing API key
|
||||
(OneUptimeApiService.initialize as jest.Mock).mockImplementation(
|
||||
(config: unknown) => {
|
||||
const typedConfig = config as { url: string; apiKey: string };
|
||||
if (!typedConfig.apiKey) {
|
||||
throw new Error("OneUptime API key is required");
|
||||
}
|
||||
}
|
||||
);
|
||||
describe("Tool Management", () => {
|
||||
it("should generate tools correctly", () => {
|
||||
const mockTools: McpToolInfo[] = [
|
||||
{
|
||||
name: "create_monitor",
|
||||
description: "Create a new monitor",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
url: { type: "string" },
|
||||
},
|
||||
required: ["name", "url"],
|
||||
},
|
||||
modelName: "Monitor",
|
||||
operation: OneUptimeOperation.Create,
|
||||
modelType: ModelType.Database,
|
||||
singularName: "monitor",
|
||||
pluralName: "monitors",
|
||||
tableName: "Monitor",
|
||||
apiPath: "/api/monitor",
|
||||
},
|
||||
{
|
||||
name: "list_projects",
|
||||
description: "List all projects",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "number" },
|
||||
skip: { type: "number" },
|
||||
},
|
||||
},
|
||||
modelName: "Project",
|
||||
operation: OneUptimeOperation.List,
|
||||
modelType: ModelType.Database,
|
||||
singularName: "project",
|
||||
pluralName: "projects",
|
||||
tableName: "Project",
|
||||
apiPath: "/api/project",
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
OneUptimeApiService.initialize({
|
||||
url: "https://test.oneuptime.com",
|
||||
apiKey: "",
|
||||
});
|
||||
}).toThrow("OneUptime API key is required");
|
||||
});
|
||||
(ToolGenerator.generateAllTools as jest.Mock).mockReturnValue(mockTools);
|
||||
|
||||
const tools: McpToolInfo[] = ToolGenerator.generateAllTools();
|
||||
|
||||
expect(tools).toHaveLength(2);
|
||||
expect(tools[0]?.name).toBe("create_monitor");
|
||||
expect(tools[1]?.name).toBe("list_projects");
|
||||
expect(tools[0]?.operation).toBe(OneUptimeOperation.Create);
|
||||
expect(tools[1]?.operation).toBe(OneUptimeOperation.List);
|
||||
});
|
||||
|
||||
describe("Tool Management", () => {
|
||||
it("should generate tools correctly", () => {
|
||||
const mockTools: McpToolInfo[] = [
|
||||
{
|
||||
name: "create_monitor",
|
||||
description: "Create a new monitor",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
url: { type: "string" },
|
||||
},
|
||||
required: ["name", "url"],
|
||||
},
|
||||
modelName: "Monitor",
|
||||
operation: OneUptimeOperation.Create,
|
||||
modelType: ModelType.Database,
|
||||
singularName: "monitor",
|
||||
pluralName: "monitors",
|
||||
tableName: "Monitor",
|
||||
apiPath: "/api/monitor",
|
||||
},
|
||||
{
|
||||
name: "list_projects",
|
||||
description: "List all projects",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "number" },
|
||||
skip: { type: "number" },
|
||||
},
|
||||
},
|
||||
modelName: "Project",
|
||||
operation: OneUptimeOperation.List,
|
||||
modelType: ModelType.Database,
|
||||
singularName: "project",
|
||||
pluralName: "projects",
|
||||
tableName: "Project",
|
||||
apiPath: "/api/project",
|
||||
},
|
||||
];
|
||||
it("should handle tool generation errors", () => {
|
||||
(ToolGenerator.generateAllTools as jest.Mock).mockImplementation(() => {
|
||||
throw new Error("Failed to generate tools");
|
||||
});
|
||||
|
||||
(ToolGenerator.generateAllTools as jest.Mock).mockReturnValue(mockTools);
|
||||
|
||||
const tools: McpToolInfo[] = ToolGenerator.generateAllTools();
|
||||
|
||||
expect(tools).toHaveLength(2);
|
||||
expect(tools[0]?.name).toBe("create_monitor");
|
||||
expect(tools[1]?.name).toBe("list_projects");
|
||||
expect(tools[0]?.operation).toBe(OneUptimeOperation.Create);
|
||||
expect(tools[1]?.operation).toBe(OneUptimeOperation.List);
|
||||
});
|
||||
|
||||
it("should handle tool generation errors", () => {
|
||||
(ToolGenerator.generateAllTools as jest.Mock).mockImplementation(() => {
|
||||
throw new Error("Failed to generate tools");
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
ToolGenerator.generateAllTools();
|
||||
}).toThrow("Failed to generate tools");
|
||||
});
|
||||
expect(() => {
|
||||
ToolGenerator.generateAllTools();
|
||||
}).toThrow("Failed to generate tools");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user