Skip to content

Commit 9bca280

Browse files
committed
feat: Add ChatGPT MCP support with search action
- Add search action handler for ChatGPT MCP compatibility - Implement /search endpoint and /.well-known/mcp-manifest - Support searching across DoiT reports, anomalies, incidents, and tickets - Maintain backward compatibility with Claude SSE implementation
1 parent dfee8aa commit 9bca280

File tree

3 files changed

+240
-0
lines changed

3 files changed

+240
-0
lines changed

doit-mcp-server/src/app.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
renderCustomerContextScreen,
1111
} from "./utils";
1212
import type { OAuthHelpers } from "@cloudflare/workers-oauth-provider";
13+
import { handleSearch, SearchArgumentsSchema } from "./chatgpt-search";
1314
import { handleValidateUserRequest } from "../../src/tools/validateUser";
1415
import { decodeJWT } from "../../src/utils/util";
1516

@@ -180,6 +181,79 @@ app.post("/customer-context", async (c) => {
180181
// then completing the authorization request with the OAUTH_PROVIDER
181182
app.post("/approve", handleApprove);
182183

184+
// ChatGPT MCP search endpoint
185+
app.post("/search", async (c) => {
186+
try {
187+
const authHeader = c.req.header("Authorization");
188+
if (!authHeader) {
189+
return c.json({ error: "Authorization header required" }, 401);
190+
}
191+
192+
// Extract token from Bearer header
193+
const token = authHeader.replace("Bearer ", "");
194+
const body = await c.req.json();
195+
196+
// Validate search arguments
197+
const args = SearchArgumentsSchema.parse(body);
198+
199+
// Get customer context from JWT or request
200+
let customerContext = body.customerContext;
201+
if (!customerContext) {
202+
const jwtInfo = decodeJWT(token);
203+
customerContext = jwtInfo?.payload?.DoitEmployee ? "EE8CtpzYiKp0dVAESVrB" : undefined;
204+
}
205+
206+
const results = await handleSearch(args, token, customerContext);
207+
return c.json(results);
208+
209+
} catch (error) {
210+
console.error("Search endpoint error:", error);
211+
return c.json({
212+
error: "Search failed",
213+
message: error instanceof Error ? error.message : "Unknown error"
214+
}, 500);
215+
}
216+
});
217+
218+
// ChatGPT MCP manifest endpoint
219+
app.get("/.well-known/mcp-manifest", (c) => {
220+
const url = new URL(c.req.url);
221+
const base = url.origin;
222+
223+
return c.json({
224+
name: "DoiT MCP Server",
225+
description: "Access DoiT platform data for cloud cost optimization and analytics",
226+
version: "1.0.0",
227+
actions: [
228+
{
229+
name: "search",
230+
description: "Search across DoiT platform data including reports, anomalies, incidents, and tickets",
231+
endpoint: `${base}/search`,
232+
method: "POST",
233+
parameters: {
234+
type: "object",
235+
properties: {
236+
query: {
237+
type: "string",
238+
description: "Search query string"
239+
},
240+
limit: {
241+
type: "number",
242+
description: "Maximum number of results to return",
243+
default: 10
244+
}
245+
},
246+
required: ["query"]
247+
}
248+
}
249+
],
250+
authentication: {
251+
type: "bearer",
252+
description: "DoiT API key required for authentication"
253+
}
254+
});
255+
});
256+
183257
// Add /.well-known/oauth-authorization-server endpoint
184258
app.get("/.well-known/oauth-authorization-server", (c) => {
185259
// Extract base URL (protocol + host)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { z } from "zod";
2+
3+
// Search arguments schema for ChatGPT MCP
4+
export const SearchArgumentsSchema = z.object({
5+
query: z.string().describe("Search query string"),
6+
limit: z.number().optional().describe("Maximum number of results to return"),
7+
});
8+
9+
export type SearchArguments = z.infer<typeof SearchArgumentsSchema>;
10+
11+
// Search result interface
12+
interface SearchResult {
13+
title: string;
14+
content: string;
15+
url?: string;
16+
metadata?: Record<string, any>;
17+
}
18+
19+
// Main search handler that ChatGPT expects
20+
export async function handleSearch(
21+
args: SearchArguments,
22+
token: string,
23+
customerContext?: string
24+
): Promise<{ results: SearchResult[] }> {
25+
const { query, limit = 10 } = args;
26+
27+
// Search across DoiT resources based on query
28+
const results: SearchResult[] = [];
29+
30+
try {
31+
// Search in reports
32+
if (query.toLowerCase().includes('report') || query.toLowerCase().includes('cost') || query.toLowerCase().includes('analytics')) {
33+
try {
34+
const { handleReportsRequest } = await import("../../src/tools/reports.js");
35+
const reportsResponse = await handleReportsRequest({ customerContext }, token);
36+
37+
if (reportsResponse.content?.[0]?.text) {
38+
const reports = JSON.parse(reportsResponse.content[0].text);
39+
reports.slice(0, Math.min(3, limit)).forEach((report: any) => {
40+
results.push({
41+
title: `Report: ${report.name || report.id}`,
42+
content: `DoiT Analytics Report - ${report.description || 'Cloud cost and usage analytics'}`,
43+
metadata: { type: 'report', id: report.id }
44+
});
45+
});
46+
}
47+
} catch (error) {
48+
console.error('Reports search error:', error);
49+
}
50+
}
51+
52+
// Search in anomalies
53+
if (query.toLowerCase().includes('anomaly') || query.toLowerCase().includes('alert') || query.toLowerCase().includes('unusual')) {
54+
try {
55+
const { handleAnomaliesRequest } = await import("../../src/tools/anomalies.js");
56+
const anomaliesResponse = await handleAnomaliesRequest({ customerContext }, token);
57+
58+
if (anomaliesResponse.content?.[0]?.text) {
59+
const anomalies = JSON.parse(anomaliesResponse.content[0].text);
60+
anomalies.slice(0, Math.min(3, limit - results.length)).forEach((anomaly: any) => {
61+
results.push({
62+
title: `Anomaly: ${anomaly.title || anomaly.id}`,
63+
content: `Cost anomaly detected - ${anomaly.description || 'Unusual spending pattern identified'}`,
64+
metadata: { type: 'anomaly', id: anomaly.id }
65+
});
66+
});
67+
}
68+
} catch (error) {
69+
console.error('Anomalies search error:', error);
70+
}
71+
}
72+
73+
// Search in cloud incidents
74+
if (query.toLowerCase().includes('incident') || query.toLowerCase().includes('issue') || query.toLowerCase().includes('outage')) {
75+
try {
76+
const { handleCloudIncidentsRequest } = await import("../../src/tools/cloudIncidents.js");
77+
const incidentsResponse = await handleCloudIncidentsRequest({ customerContext }, token);
78+
79+
if (incidentsResponse.content?.[0]?.text) {
80+
const incidents = JSON.parse(incidentsResponse.content[0].text);
81+
incidents.slice(0, Math.min(3, limit - results.length)).forEach((incident: any) => {
82+
results.push({
83+
title: `Incident: ${incident.title || incident.id}`,
84+
content: `Cloud service incident - ${incident.description || 'Service disruption or issue'}`,
85+
metadata: { type: 'incident', id: incident.id }
86+
});
87+
});
88+
}
89+
} catch (error) {
90+
console.error('Incidents search error:', error);
91+
}
92+
}
93+
94+
// Search in tickets
95+
if (query.toLowerCase().includes('ticket') || query.toLowerCase().includes('support')) {
96+
try {
97+
const { handleListTicketsRequest } = await import("../../src/tools/tickets.js");
98+
const ticketsResponse = await handleListTicketsRequest({ customerContext }, token);
99+
100+
if (ticketsResponse.content?.[0]?.text) {
101+
const tickets = JSON.parse(ticketsResponse.content[0].text);
102+
tickets.slice(0, Math.min(3, limit - results.length)).forEach((ticket: any) => {
103+
results.push({
104+
title: `Ticket: ${ticket.subject || ticket.id}`,
105+
content: `Support ticket - ${ticket.description || 'Customer support request'}`,
106+
metadata: { type: 'ticket', id: ticket.id }
107+
});
108+
});
109+
}
110+
} catch (error) {
111+
console.error('Tickets search error:', error);
112+
}
113+
}
114+
115+
// If no specific matches, provide general DoiT information
116+
if (results.length === 0) {
117+
results.push({
118+
title: "DoiT Platform Overview",
119+
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'.`,
120+
metadata: { type: 'general' }
121+
});
122+
}
123+
124+
} catch (error) {
125+
console.error('Search error:', error);
126+
results.push({
127+
title: "Search Error",
128+
content: `Unable to complete search for "${query}". Please check your authentication and try again.`,
129+
metadata: { type: 'error' }
130+
});
131+
}
132+
133+
return { results: results.slice(0, limit) };
134+
}
135+
136+
// Export the search tool definition
137+
export const searchTool = {
138+
name: "search",
139+
description: "Search across DoiT platform data including reports, anomalies, incidents, and tickets",
140+
};

doit-mcp-server/src/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ import {
5757
ListAssetsArgumentsSchema,
5858
listAssetsTool,
5959
} from "../../src/tools/assets.js";
60+
import {
61+
SearchArgumentsSchema,
62+
searchTool,
63+
handleSearch,
64+
} from "./chatgpt-search.js";
6065
import {
6166
ChangeCustomerArgumentsSchema,
6267
changeCustomerTool,
@@ -166,6 +171,19 @@ export class DoitMCPAgent extends McpAgent {
166171
};
167172
}
168173

174+
// Special callback for search tool (ChatGPT compatibility)
175+
private createSearchCallback() {
176+
return async (args: any) => {
177+
const token = this.getToken();
178+
const persistedCustomerContext = await this.loadPersistedProps();
179+
const customerContext =
180+
persistedCustomerContext || (this.props.customerContext as string);
181+
182+
const response = await handleSearch(args, token, customerContext);
183+
return convertToMcpResponse(response);
184+
};
185+
}
186+
169187
// Special callback for changeCustomer tool
170188
private createChangeCustomerCallback() {
171189
return async (args: any) => {
@@ -264,6 +282,14 @@ export class DoitMCPAgent extends McpAgent {
264282
// Assets tools
265283
this.registerTool(listAssetsTool, ListAssetsArgumentsSchema);
266284

285+
// Search tool (ChatGPT compatibility)
286+
(this.server.tool as any)(
287+
searchTool.name,
288+
searchTool.description,
289+
zodSchemaToMcpTool(SearchArgumentsSchema),
290+
this.createSearchCallback()
291+
);
292+
267293
// Change Customer tool (requires special handling)
268294
if (this.props.isDoitUser === "true") {
269295
(this.server.tool as any)(

0 commit comments

Comments
 (0)