api.searchEndpoints
Search API documentation to find endpoints by path, method, parameters, or tags. Returns essential details for making API calls and suggests live testing when schemas are incomplete.
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 |
|---|---|---|---|
| query | No | Text query to match against path, summary, description, operationId, and tags | |
| tag | No | Filter by a specific tag (case-insensitive exact match) | |
| searchTerms | No | [DEPRECATED] Previous array of keywords. If provided, behaves like an OR search across terms. | |
| method | No | Filter results by HTTP method (optional) | |
| limit | No | Maximum number of endpoints to return (default: 10) | |
| maxResults | No | [DEPRECATED] Use 'limit' instead. |
Implementation Reference
- browser-tools-mcp/mcp-server.ts:1474-1619 (registration)MCP tool registration for 'api.searchEndpoints', including input schema (Zod) and inline handler that proxies requests to browser-tools-server's /api/embed/search endpoint for semantic search.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, }; } } );
- Express.js HTTP handler for POST /api/embed/search endpoint. Parses request (with X-ACTIVE-PROJECT header for project context), calls semantic-index.searchSemantic(), returns search results. Called by MCP tool handler.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 implementation: embeds search query/tags/method hints, similarity-searches pre-built vector index of API endpoints, post-filters, enriches results with live Swagger schema types (request/response/requiresAuth).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; }
- Helper extracts schema type info (contentType, type/$ref/enum etc.) and auth requirements from Swagger for search results.function hydrateOperationTypes(swagger: any, method: string, apiPath: string) { const p = swagger.paths?.[apiPath]; const op = p?.[method.toLowerCase()]; if (!op) return { request: {}, response: {}, requiresAuth: false }; // Request body let request: { contentType?: string; schemaType?: string } = {}; const rb = op.requestBody?.content; if (rb && typeof rb === "object") { const ct = rb["application/json"] ? "application/json" : Object.keys(rb)[0]; if (ct) { request.contentType = ct; request.schemaType = resolveSchemaType(rb[ct]?.schema); } } // Response (prefer 200, else first) let response: { status?: string; contentType?: string; schemaType?: string } = {}; const resps = op.responses || {}; const preferred = resps["200"] ? "200" : Object.keys(resps)[0]; if (preferred) { response.status = preferred; const content = resps[preferred]?.content; if (content && typeof content === "object") { const ct = content["application/json"] ? "application/json" : Object.keys(content)[0]; if (ct) { response.contentType = ct; response.schemaType = resolveSchemaType(content[ct]?.schema); } } } // Determine if auth is required using OpenAPI security objects const hasAuth = (sec: any): boolean => { if (!Array.isArray(sec)) return false; for (const req of sec) { if (req && typeof req === "object" && Object.keys(req).length > 0) return true; } return false; }; const requiresAuth = op.security !== undefined ? hasAuth(op.security) : hasAuth(swagger.security); return { request, response, requiresAuth }; }
- Zod input schema validation for api.searchEndpoints tool parameters.{ 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."), },