Repository navigation
feat(mcp): add omniroute_web_search tool with execute:search scope #884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -394,6 +394,59 @@ export const listModelsCatalogTool: McpToolDefinition< | |||||||||||||||||
| sourceEndpoints: ["/api/models/catalog", "/v1/models"], | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| // --- Tool 9: omniroute_web_search --- | ||||||||||||||||||
| export const webSearchInput = z.object({ | ||||||||||||||||||
| query: z | ||||||||||||||||||
| .string() | ||||||||||||||||||
| .min(1, "Query is required") | ||||||||||||||||||
| .max(1000, "Query must be 1000 characters or fewer") | ||||||||||||||||||
| .describe("The search query string"), | ||||||||||||||||||
| max_results: z | ||||||||||||||||||
| .number() | ||||||||||||||||||
| .int() | ||||||||||||||||||
| .min(1) | ||||||||||||||||||
| .max(20) | ||||||||||||||||||
| .default(5) | ||||||||||||||||||
| .describe("Maximum number of search results to return"), | ||||||||||||||||||
| search_type: z.enum(["web", "news"]).default("web").describe("Type of search to perform"), | ||||||||||||||||||
| provider: z | ||||||||||||||||||
| .string() | ||||||||||||||||||
| .optional() | ||||||||||||||||||
| .describe("Specific search provider to use (serper, brave, perplexity, exa, tavily)"), | ||||||||||||||||||
|
Comment on lines
+412
to
+415
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| export const webSearchOutput = z.object({ | ||||||||||||||||||
| id: z.string(), | ||||||||||||||||||
| provider: z.string(), | ||||||||||||||||||
| query: z.string(), | ||||||||||||||||||
| results: z.array( | ||||||||||||||||||
| z.object({ | ||||||||||||||||||
| title: z.string(), | ||||||||||||||||||
| url: z.string(), | ||||||||||||||||||
| display_url: z.string().optional(), | ||||||||||||||||||
| snippet: z.string(), | ||||||||||||||||||
| position: z.number().int().positive(), | ||||||||||||||||||
| }) | ||||||||||||||||||
| ), | ||||||||||||||||||
| cached: z.boolean(), | ||||||||||||||||||
| usage: z.object({ | ||||||||||||||||||
| queries_used: z.number().int().min(0), | ||||||||||||||||||
| search_cost_usd: z.number().min(0), | ||||||||||||||||||
| }), | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+418
to
+436
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The export const webSearchOutput = z.object({
id: z.string(),
provider: z.string(),
query: z.string(),
results: z.array(
z.object({
title: z.string(),
url: z.string(),
display_url: z.string().optional(),
snippet: z.string(),
position: z.number().int().positive(),
})
),
answer: z
.object({
text: z.string().nullable(),
source: z.string().optional(),
})
.nullable()
.optional(),
cached: z.boolean(),
usage: z.object({
queries_used: z.number().int().min(0),
search_cost_usd: z.number().min(0),
}),
}); |
||||||||||||||||||
|
|
||||||||||||||||||
| export const webSearchTool: McpToolDefinition<typeof webSearchInput, typeof webSearchOutput> = { | ||||||||||||||||||
| name: "omniroute_web_search", | ||||||||||||||||||
| description: | ||||||||||||||||||
| "Performs a web search using OmniRoute's search gateway. Supports multiple providers (Serper, Brave, Perplexity, Exa, Tavily) with automatic failover. Returns search results with titles, URLs, snippets, and position data.", | ||||||||||||||||||
| inputSchema: webSearchInput, | ||||||||||||||||||
| outputSchema: webSearchOutput, | ||||||||||||||||||
| scopes: ["execute:search"], | ||||||||||||||||||
| auditLevel: "basic", | ||||||||||||||||||
| phase: 1, | ||||||||||||||||||
| sourceEndpoints: ["/v1/search"], | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| // ============ Phase 2: Advanced Tools (8) ============ | ||||||||||||||||||
|
|
||||||||||||||||||
| // --- Tool 9: omniroute_simulate_route --- | ||||||||||||||||||
|
|
@@ -881,6 +934,7 @@ export const MCP_TOOLS = [ | |||||||||||||||||
| routeRequestTool, | ||||||||||||||||||
| costReportTool, | ||||||||||||||||||
| listModelsCatalogTool, | ||||||||||||||||||
| webSearchTool, | ||||||||||||||||||
| simulateRouteTool, | ||||||||||||||||||
| setBudgetGuardTool, | ||||||||||||||||||
| setRoutingStrategyTool, | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ import { | |||||
| routeRequestInput, | ||||||
| costReportInput, | ||||||
| listModelsCatalogInput, | ||||||
| webSearchInput, | ||||||
| simulateRouteInput, | ||||||
| setBudgetGuardInput, | ||||||
| setRoutingStrategyInput, | ||||||
|
|
@@ -492,6 +493,37 @@ async function handleListModelsCatalog(args: { provider?: string; capability?: s | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| async function handleWebSearch(args: { | ||||||
| query: string; | ||||||
| max_results?: number; | ||||||
| search_type?: "web" | "news"; | ||||||
| provider?: string; | ||||||
| }) { | ||||||
| const start = Date.now(); | ||||||
| try { | ||||||
| const body: Record<string, unknown> = { | ||||||
| query: args.query, | ||||||
| max_results: args.max_results ?? 5, | ||||||
| search_type: args.search_type ?? "web", | ||||||
| }; | ||||||
| if (args.provider) { | ||||||
| body["provider"] = args.provider; | ||||||
| } | ||||||
|
Comment on lines
+504
to
+511
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nullish coalescing operators ( const body: Record<string, unknown> = {
query: args.query,
max_results: args.max_results,
search_type: args.search_type,
...(args.provider ? { provider: args.provider } : {}),
}; |
||||||
|
|
||||||
| const data = await omniRouteFetch("/v1/search", { | ||||||
| method: "POST", | ||||||
| body: JSON.stringify(body), | ||||||
| }); | ||||||
|
|
||||||
| await logToolCall("omniroute_web_search", args, data, Date.now() - start, true); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logging the entire
Suggested change
|
||||||
| return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] }; | ||||||
| } catch (err) { | ||||||
| const msg = err instanceof Error ? err.message : String(err); | ||||||
| await logToolCall("omniroute_web_search", args, null, Date.now() - start, false, msg); | ||||||
| return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ============ MCP Server Setup ============ | ||||||
|
|
||||||
| /** | ||||||
|
|
@@ -595,6 +627,18 @@ export function createMcpServer(): McpServer { | |||||
| ) | ||||||
| ); | ||||||
|
|
||||||
| server.registerTool( | ||||||
| "omniroute_web_search", | ||||||
| { | ||||||
| description: | ||||||
| "Performs a web search using OmniRoute's search gateway. Supports multiple providers (Serper, Brave, Perplexity, Exa, Tavily) with automatic failover. Returns search results with titles, URLs, snippets, and position data.", | ||||||
| inputSchema: webSearchInput, | ||||||
| }, | ||||||
| withScopeEnforcement("omniroute_web_search", (args) => | ||||||
| handleWebSearch(webSearchInput.parse(args)) | ||||||
| ) | ||||||
| ); | ||||||
|
|
||||||
| // ── Advanced Tools (Phase 3) ────────────────────────────── | ||||||
|
|
||||||
| server.registerTool( | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests do not actually execute the
handleWebSearchfunction or the MCP tool logic. They mockfetchand then call the mock directly (e.g., at line 168), which only verifies that the mock returns what it was given. Furthermore, the tests useGETrequests with query parameters, while the actual implementation inserver.tsuses aPOSTrequest with a JSON body. To provide meaningful coverage, the tests should call the handler (which would require exporting it) or use theserver.executeToolmethod on a server instance.