Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions doit-mcp-server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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)
Expand Down
140 changes: 140 additions & 0 deletions doit-mcp-server/src/chatgpt-search.ts
Original file line number Diff line number Diff line change
@@ -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<typeof SearchArgumentsSchema>;

// Search result interface
interface SearchResult {
title: string;
content: string;
url?: string;
metadata?: Record<string, any>;
}

// 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",
};
26 changes: 26 additions & 0 deletions doit-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ import {
ListAssetsArgumentsSchema,
listAssetsTool,
} from "../../src/tools/assets.js";
import {
SearchArgumentsSchema,
searchTool,
handleSearch,
} from "./chatgpt-search.js";
import {
ChangeCustomerArgumentsSchema,
changeCustomerTool,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)(
Expand Down