api.searchEndpoints
Search API documentation by query, tag, or method to retrieve path, parameters, request body, and success responses. Use to quickly locate endpoints or test missing schemas with 'api.request'.
Instructions
Semantic API documentation search returning essential info: path, method, params (GET), request body (POST/PUT/PATCH/DELETE), and success responses. If schemas are missing, suggests using 'api.request' for live testing.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum number of endpoints to return (default: 10) | |
| maxResults | No | [DEPRECATED] Use 'limit' instead. | |
| method | No | Filter results by HTTP method (optional) | |
| query | No | Text query to match against path, summary, description, operationId, and tags | |
| searchTerms | No | [DEPRECATED] Previous array of keywords. If provided, behaves like an OR search across terms. | |
| tag | No | Filter by a specific tag (case-insensitive exact match) |
Implementation Reference
- browser-tools-mcp/mcp-server.ts:1474-1620 (registration)Registers the MCP tool 'api.searchEndpoints' with Zod input schema and inline async handler function. The handler proxies search parameters to the browser-tools-server /api/embed/search endpoint and returns formatted results.server.tool( "api.searchEndpoints", "Semantic API documentation search returning essential info: path, method, params (GET), request body (POST/PUT/PATCH/DELETE), and success responses. If schemas are missing, suggests using 'api.request' for live testing.", { query: z .string() .optional() .describe( "Text query to match against path, summary, description, operationId, and tags" ), tag: z .string() .optional() .describe("Filter by a specific tag (case-insensitive exact match)"), // Backward-compatibility: deprecated. Prefer 'query' or 'tag'. searchTerms: z .array(z.string()) .optional() .describe( "[DEPRECATED] Previous array of keywords. If provided, behaves like an OR search across terms." ), method: z .enum(["GET", "POST", "PUT", "PATCH", "DELETE"]) .optional() .describe("Filter results by HTTP method (optional)"), limit: z .number() .optional() .default(10) .describe("Maximum number of endpoints to return (default: 10)"), // Backward-compatibility alias for 'limit' maxResults: z .number() .optional() .describe("[DEPRECATED] Use 'limit' instead."), }, async (params) => { try { const { query, tag, method } = params as any; const limit = (params as any).limit ?? (params as any).maxResults ?? 10; // Derive effective query/tag from deprecated searchTerms if needed const terms: string[] | undefined = Array.isArray( (params as any).searchTerms ) ? ((params as any).searchTerms as string[]).filter( (t) => typeof t === "string" && t.trim().length > 0 ) : undefined; // If coming from deprecated searchTerms, build an OR-regex of escaped terms const effectiveQueryIsRegex = !query && !!terms && terms.length > 0; const effectiveQuery = query ?? (terms && terms.length > 0 ? terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") : undefined); const effectiveTag = tag; // Validate filters: require at least one of query or tag if (!effectiveQuery && !effectiveTag) { return { content: [ { type: "text", text: "Provide 'query' and/or 'tag' to search.", }, ], isError: true, }; } // Call backend semantic search endpoint. Pass implicit project via header (no param changes for tool callers). const payload = { query: effectiveQuery, tag: effectiveTag, method, limit, } as any; const apiResult = await withServerConnection(async () => { const activeProjectHeader = getActiveProjectName(); const resp = await fetch( `http://${discoveredHost}:${discoveredPort}/api/embed/search`, { method: "POST", headers: { "Content-Type": "application/json", ...(activeProjectHeader ? { "X-ACTIVE-PROJECT": activeProjectHeader } : {}), }, body: JSON.stringify(payload), } ); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return await resp.json(); }); // If withServerConnection returned an MCP-style error object, pass it through if (!Array.isArray(apiResult) && (apiResult as any)?.content) { return apiResult as any; } const endpoints = Array.isArray(apiResult) ? apiResult : []; const result = { summary: { totalFound: endpoints.length, filter: effectiveQuery && effectiveTag ? { type: "mixed", value: `${effectiveQuery} (tag: ${effectiveTag})`, } : effectiveQuery ? { type: "query", value: effectiveQuery } : { type: "tag", value: effectiveTag }, methodFilter: method || "all", }, endpoints, // Hint for callers: include requiresAuth boolean if present note: "Each endpoint may include 'requiresAuth' derived from OpenAPI security. If true, call api_request with includeAuthToken: true.", }; return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error: any) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Failed to search API documentation: ${errorMessage}`, }, ], isError: true, }; } } );
- Executes the tool logic: validates/normalizes params, discovers browser server if needed, POSTs to /api/embed/search semantic search endpoint, formats results with summary/metadata, returns MCP content response.async (params) => { try { const { query, tag, method } = params as any; const limit = (params as any).limit ?? (params as any).maxResults ?? 10; // Derive effective query/tag from deprecated searchTerms if needed const terms: string[] | undefined = Array.isArray( (params as any).searchTerms ) ? ((params as any).searchTerms as string[]).filter( (t) => typeof t === "string" && t.trim().length > 0 ) : undefined; // If coming from deprecated searchTerms, build an OR-regex of escaped terms const effectiveQueryIsRegex = !query && !!terms && terms.length > 0; const effectiveQuery = query ?? (terms && terms.length > 0 ? terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") : undefined); const effectiveTag = tag; // Validate filters: require at least one of query or tag if (!effectiveQuery && !effectiveTag) { return { content: [ { type: "text", text: "Provide 'query' and/or 'tag' to search.", }, ], isError: true, }; } // Call backend semantic search endpoint. Pass implicit project via header (no param changes for tool callers). const payload = { query: effectiveQuery, tag: effectiveTag, method, limit, } as any; const apiResult = await withServerConnection(async () => { const activeProjectHeader = getActiveProjectName(); const resp = await fetch( `http://${discoveredHost}:${discoveredPort}/api/embed/search`, { method: "POST", headers: { "Content-Type": "application/json", ...(activeProjectHeader ? { "X-ACTIVE-PROJECT": activeProjectHeader } : {}), }, body: JSON.stringify(payload), } ); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return await resp.json(); }); // If withServerConnection returned an MCP-style error object, pass it through if (!Array.isArray(apiResult) && (apiResult as any)?.content) { return apiResult as any; } const endpoints = Array.isArray(apiResult) ? apiResult : []; const result = { summary: { totalFound: endpoints.length, filter: effectiveQuery && effectiveTag ? { type: "mixed", value: `${effectiveQuery} (tag: ${effectiveTag})`, } : effectiveQuery ? { type: "query", value: effectiveQuery } : { type: "tag", value: effectiveTag }, methodFilter: method || "all", }, endpoints, // Hint for callers: include requiresAuth boolean if present note: "Each endpoint may include 'requiresAuth' derived from OpenAPI security. If true, call api_request with includeAuthToken: true.", }; return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error: any) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Failed to search API documentation: ${errorMessage}`, }, ], isError: true, }; } }
- Zod input schema defining parameters for the tool: query/tag filters, method, limit (supports deprecated params for compatibility).{ query: z .string() .optional() .describe( "Text query to match against path, summary, description, operationId, and tags" ), tag: z .string() .optional() .describe("Filter by a specific tag (case-insensitive exact match)"), // Backward-compatibility: deprecated. Prefer 'query' or 'tag'. searchTerms: z .array(z.string()) .optional() .describe( "[DEPRECATED] Previous array of keywords. If provided, behaves like an OR search across terms." ), method: z .enum(["GET", "POST", "PUT", "PATCH", "DELETE"]) .optional() .describe("Filter results by HTTP method (optional)"), limit: z .number() .optional() .default(10) .describe("Maximum number of endpoints to return (default: 10)"), // Backward-compatibility alias for 'limit' maxResults: z .number() .optional() .describe("[DEPRECATED] Use 'limit' instead."), },
- HTTP POST endpoint handler (/api/embed/search) called by MCP tool. Extracts project from header/body, invokes semantic-index searchSemantic, returns results.app.post("/api/embed/search", async (req, res) => { try { const projectHeader = (req.header("X-ACTIVE-PROJECT") as string) || (req.header("active-project") as string) || undefined; const { query, tag, method, limit } = req.body || {}; // Short info log logInfo( `[embed] Search request project=${projectHeader || "<default>"} query=${ query ? "yes" : "no" } tag=${tag || "none"} method=${method || "all"} limit=${ typeof limit === "number" ? limit : 10 }` ); // Detailed debug log logDebug("[embed] Search request details:", { headers: { xActiveProject: req.header("X-ACTIVE-PROJECT"), activeProject: req.header("active-project"), }, body: req.body, }); const results = await searchSemantic( { query, tag, method, limit }, projectHeader ); res.json(results); } catch (error: any) { console.error("[error] /api/embed/search failed:", error); res .status(500) .json({ error: error?.message || "Failed to perform embed search" }); } });
- Core semantic search: embeds query using Gemini/OpenAI, vector search on pre-built index of Swagger endpoints, post-filters by method/tag, hydrates schema types/auth reqs from live Swagger doc.export async function searchSemantic( params: SearchParams, projectOverride?: string ): Promise<SearchResultItem[]> { const project = getProjectName(projectOverride); // Prefer per-request override if (project) { console.log(`[search] Using project "${project}" for semantic API search`); } const status = await getStatus(project); if (!status.exists) { throw new Error("Semantic index not built. Open Dev Panel and re-index."); } const meta = status.meta!; const needDims = getProviderDims(); const needModel = getProviderModel(); if (meta.dims !== needDims || meta.model !== needModel) { console.warn( `[warn] Index settings mismatch: have model=${meta.model} dims=${meta.dims}, need model=${needModel} dims=${needDims}` ); throw new Error( "Semantic index was built with different embedding settings. Please open the Dev Panel and Reindex for the current provider/model." ); } const idx = await getOrCreateIndex(project); const swagger = await getProjectSwagger(project); const qStr = buildQueryString({ query: params.query, tag: params.tag, method: params.method }); console.log(`[search] Embedding query provider=${resolveEmbeddingProvider()} model=${getProviderModel()}`); const queryChars = (params.query || "").length; const t0 = Date.now(); const [qVec] = await embedBatch([qStr], getProviderDims(), "RETRIEVAL_QUERY"); const elapsedMs = Date.now() - t0; console.log( `[search] Query embed completed timeMs=${elapsedMs} queryChars=${queryChars} builtChars=${qStr.length} provider=${resolveEmbeddingProvider()} model=${getProviderModel()}` ); const pool = await idx.queryItems(qVec, "", TOPK); const method = params.method ? params.method.toUpperCase() : undefined; const tag = params.tag; const methodTagMatch = (md: any) => { const methodOk = method ? md.method === method : true; let tagOk = true; if (tag) { if (Array.isArray(md.tags)) { tagOk = md.tags.includes(tag); } else if (typeof md.tags === "string") { const parts = md.tags.split(",").map((s: string) => s.trim()).filter(Boolean); tagOk = parts.includes(tag); } else { tagOk = false; } } return methodOk && tagOk; }; const filtered = pool.filter((r: any) => methodTagMatch(r.item.metadata)); // Backfill if needed const needed = (params.limit ?? DEFAULT_LIMIT) - filtered.length; let candidates: any[] = filtered; if (needed > 0) { const extras = pool.filter( (r: any) => !filtered.find((f: any) => f.item.metadata.path === r.item.metadata.path && f.item.metadata.method === r.item.metadata.method) ); candidates = [...filtered, ...extras.slice(0, needed)]; } const top = candidates.slice(0, params.limit ?? DEFAULT_LIMIT); // Hydrate request/response minimal types from Swagger const results: SearchResultItem[] = top.map((r: any) => { const md = r.item.metadata || {}; const { request, response, requiresAuth } = hydrateOperationTypes(swagger, md.method, md.path); return { method: md.method, path: md.path, request, response, requiresAuth, }; }); return results; }