diff --git a/doit-mcp-server/src/app.ts b/doit-mcp-server/src/app.ts index 28cf8b2..ec8b87b 100644 --- a/doit-mcp-server/src/app.ts +++ b/doit-mcp-server/src/app.ts @@ -10,6 +10,7 @@ import { renderCustomerContextScreen, } from "./utils"; import type { OAuthHelpers } from "@cloudflare/workers-oauth-provider"; +import { handleSearch, SearchArgumentsSchema } from "./chatgpt-search"; import { handleValidateUserRequest } from "../../src/tools/validateUser"; import { decodeJWT } from "../../src/utils/util"; @@ -180,6 +181,79 @@ app.post("/customer-context", async (c) => { // then completing the authorization request with the OAUTH_PROVIDER app.post("/approve", handleApprove); +// ChatGPT MCP search endpoint +app.post("/search", async (c) => { + try { + const authHeader = c.req.header("Authorization"); + if (!authHeader) { + return c.json({ error: "Authorization header required" }, 401); + } + + // Extract token from Bearer header + const token = authHeader.replace("Bearer ", ""); + const body = await c.req.json(); + + // Validate search arguments + const args = SearchArgumentsSchema.parse(body); + + // Get customer context from JWT or request + let customerContext = body.customerContext; + if (!customerContext) { + const jwtInfo = decodeJWT(token); + customerContext = jwtInfo?.payload?.DoitEmployee ? "EE8CtpzYiKp0dVAESVrB" : undefined; + } + + const results = await handleSearch(args, token, customerContext); + return c.json(results); + + } catch (error) { + console.error("Search endpoint error:", error); + return c.json({ + error: "Search failed", + message: error instanceof Error ? error.message : "Unknown error" + }, 500); + } +}); + +// ChatGPT MCP manifest endpoint +app.get("/.well-known/mcp-manifest", (c) => { + const url = new URL(c.req.url); + const base = url.origin; + + return c.json({ + name: "DoiT MCP Server", + description: "Access DoiT platform data for cloud cost optimization and analytics", + version: "1.0.0", + actions: [ + { + name: "search", + description: "Search across DoiT platform data including reports, anomalies, incidents, and tickets", + endpoint: `${base}/search`, + method: "POST", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query string" + }, + limit: { + type: "number", + description: "Maximum number of results to return", + default: 10 + } + }, + required: ["query"] + } + } + ], + authentication: { + type: "bearer", + description: "DoiT API key required for authentication" + } + }); +}); + // Add /.well-known/oauth-authorization-server endpoint app.get("/.well-known/oauth-authorization-server", (c) => { // Extract base URL (protocol + host) diff --git a/doit-mcp-server/src/chatgpt-search.ts b/doit-mcp-server/src/chatgpt-search.ts new file mode 100644 index 0000000..732752d --- /dev/null +++ b/doit-mcp-server/src/chatgpt-search.ts @@ -0,0 +1,140 @@ +import { z } from "zod"; + +// Search arguments schema for ChatGPT MCP +export const SearchArgumentsSchema = z.object({ + query: z.string().describe("Search query string"), + limit: z.number().optional().describe("Maximum number of results to return"), +}); + +export type SearchArguments = z.infer; + +// Search result interface +interface SearchResult { + title: string; + content: string; + url?: string; + metadata?: Record; +} + +// Main search handler that ChatGPT expects +export async function handleSearch( + args: SearchArguments, + token: string, + customerContext?: string +): Promise<{ results: SearchResult[] }> { + const { query, limit = 10 } = args; + + // Search across DoiT resources based on query + const results: SearchResult[] = []; + + try { + // Search in reports + if (query.toLowerCase().includes('report') || query.toLowerCase().includes('cost') || query.toLowerCase().includes('analytics')) { + try { + const { handleReportsRequest } = await import("../../src/tools/reports.js"); + const reportsResponse = await handleReportsRequest({ customerContext }, token); + + if (reportsResponse.content?.[0]?.text) { + const reports = JSON.parse(reportsResponse.content[0].text); + reports.slice(0, Math.min(3, limit)).forEach((report: any) => { + results.push({ + title: `Report: ${report.name || report.id}`, + content: `DoiT Analytics Report - ${report.description || 'Cloud cost and usage analytics'}`, + metadata: { type: 'report', id: report.id } + }); + }); + } + } catch (error) { + console.error('Reports search error:', error); + } + } + + // Search in anomalies + if (query.toLowerCase().includes('anomaly') || query.toLowerCase().includes('alert') || query.toLowerCase().includes('unusual')) { + try { + const { handleAnomaliesRequest } = await import("../../src/tools/anomalies.js"); + const anomaliesResponse = await handleAnomaliesRequest({ customerContext }, token); + + if (anomaliesResponse.content?.[0]?.text) { + const anomalies = JSON.parse(anomaliesResponse.content[0].text); + anomalies.slice(0, Math.min(3, limit - results.length)).forEach((anomaly: any) => { + results.push({ + title: `Anomaly: ${anomaly.title || anomaly.id}`, + content: `Cost anomaly detected - ${anomaly.description || 'Unusual spending pattern identified'}`, + metadata: { type: 'anomaly', id: anomaly.id } + }); + }); + } + } catch (error) { + console.error('Anomalies search error:', error); + } + } + + // Search in cloud incidents + if (query.toLowerCase().includes('incident') || query.toLowerCase().includes('issue') || query.toLowerCase().includes('outage')) { + try { + const { handleCloudIncidentsRequest } = await import("../../src/tools/cloudIncidents.js"); + const incidentsResponse = await handleCloudIncidentsRequest({ customerContext }, token); + + if (incidentsResponse.content?.[0]?.text) { + const incidents = JSON.parse(incidentsResponse.content[0].text); + incidents.slice(0, Math.min(3, limit - results.length)).forEach((incident: any) => { + results.push({ + title: `Incident: ${incident.title || incident.id}`, + content: `Cloud service incident - ${incident.description || 'Service disruption or issue'}`, + metadata: { type: 'incident', id: incident.id } + }); + }); + } + } catch (error) { + console.error('Incidents search error:', error); + } + } + + // Search in tickets + if (query.toLowerCase().includes('ticket') || query.toLowerCase().includes('support')) { + try { + const { handleListTicketsRequest } = await import("../../src/tools/tickets.js"); + const ticketsResponse = await handleListTicketsRequest({ customerContext }, token); + + if (ticketsResponse.content?.[0]?.text) { + const tickets = JSON.parse(ticketsResponse.content[0].text); + tickets.slice(0, Math.min(3, limit - results.length)).forEach((ticket: any) => { + results.push({ + title: `Ticket: ${ticket.subject || ticket.id}`, + content: `Support ticket - ${ticket.description || 'Customer support request'}`, + metadata: { type: 'ticket', id: ticket.id } + }); + }); + } + } catch (error) { + console.error('Tickets search error:', error); + } + } + + // If no specific matches, provide general DoiT information + if (results.length === 0) { + results.push({ + title: "DoiT Platform Overview", + content: `DoiT provides cloud cost optimization, analytics, and support services. Available data includes cost reports, anomaly detection, cloud incidents, and support tickets. Try searching for specific terms like 'reports', 'anomalies', 'incidents', or 'tickets'.`, + metadata: { type: 'general' } + }); + } + + } catch (error) { + console.error('Search error:', error); + results.push({ + title: "Search Error", + content: `Unable to complete search for "${query}". Please check your authentication and try again.`, + metadata: { type: 'error' } + }); + } + + return { results: results.slice(0, limit) }; +} + +// Export the search tool definition +export const searchTool = { + name: "search", + description: "Search across DoiT platform data including reports, anomalies, incidents, and tickets", +}; diff --git a/doit-mcp-server/src/index.ts b/doit-mcp-server/src/index.ts index 67a8e01..95ec2ff 100644 --- a/doit-mcp-server/src/index.ts +++ b/doit-mcp-server/src/index.ts @@ -57,6 +57,11 @@ import { ListAssetsArgumentsSchema, listAssetsTool, } from "../../src/tools/assets.js"; +import { + SearchArgumentsSchema, + searchTool, + handleSearch, +} from "./chatgpt-search.js"; import { ChangeCustomerArgumentsSchema, changeCustomerTool, @@ -166,6 +171,19 @@ export class DoitMCPAgent extends McpAgent { }; } + // Special callback for search tool (ChatGPT compatibility) + private createSearchCallback() { + return async (args: any) => { + const token = this.getToken(); + const persistedCustomerContext = await this.loadPersistedProps(); + const customerContext = + persistedCustomerContext || (this.props.customerContext as string); + + const response = await handleSearch(args, token, customerContext); + return convertToMcpResponse(response); + }; + } + // Special callback for changeCustomer tool private createChangeCustomerCallback() { return async (args: any) => { @@ -264,6 +282,14 @@ export class DoitMCPAgent extends McpAgent { // Assets tools this.registerTool(listAssetsTool, ListAssetsArgumentsSchema); + // Search tool (ChatGPT compatibility) + (this.server.tool as any)( + searchTool.name, + searchTool.description, + zodSchemaToMcpTool(SearchArgumentsSchema), + this.createSearchCallback() + ); + // Change Customer tool (requires special handling) if (this.props.isDoitUser === "true") { (this.server.tool as any)(