search_ecosystem
Search verified AI ecosystem content ranked by relevance. Filter by specific ecosystem or omit to search all.
Instructions
Search across all verified AI ecosystem content. Returns results ranked by relevance. Leave ecosystem blank to search across all ecosystems. Strata provides intelligence, not ground truth. Always verify critical decisions against the source_urls returned with each item.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | ||
| ecosystem | No |
Implementation Reference
- lib/mcp-tools.ts:335-372 (handler)Handler function for the 'search_ecosystem' tool. Accepts 'query' (required) and 'ecosystem' (optional) args. Optionally validates ecosystem access via checkEcosystemAccess, then calls the 'search_content_items' Postgres RPC to get ranked results across all categories. Returns items with id, title, body, category, ecosystem_slug, and source_urls.
if (name === 'search_ecosystem') { const query = (args.query as string).slice(0, 2000) const ecosystem = args.ecosystem as string | undefined const logEcosystem = ecosystem ?? 'all' let resolvedEcosystem: string | null = null if (ecosystem) { const access = await checkEcosystemAccess(supabase, ecosystem, profile.tier) if (!access.ok) { await logApiRequest(supabase, { apiKey: profile.api_key, tool: 'search', ecosystem: logEcosystem, statusCode: access.response.status }) return err('Error: Ecosystem not available on free tier. Upgrade at usestrata.dev/dashboard/billing', access.response.status) } resolvedEcosystem = access.slug } const { data, error } = await supabase.rpc('search_content_items', { search_query: query, filter_ecosystem: resolvedEcosystem, filter_category: null, user_tier: profile.tier, }) if (error) { await logApiRequest(supabase, { apiKey: profile.api_key, tool: 'search', ecosystem: logEcosystem, statusCode: 500 }) return err('Error: Search error', 500) } type SearchRow = { id: string; title: string; body: string; category: string; ecosystem_slug: string; source_url: string | null } const rows = ((data ?? []) as SearchRow[]) const results = rows.map((r) => ({ id: r.id, title: r.title, body: r.body, category: r.category, ecosystem_slug: r.ecosystem_slug, source_urls: r.source_url ? [r.source_url] : [], })) await logApiRequest(supabase, { apiKey: profile.api_key, tool: 'search', ecosystem: logEcosystem, statusCode: 200 }) return ok({ query, results }, rows.map(r => r.id)) } - lib/mcp-tools.ts:83-100 (schema)Tool definition and input schema for search_ecosystem. Has name 'search_ecosystem', description 'Search across all verified AI ecosystem content', inputSchema requiring 'query' (string) and optional 'ecosystem' (string slug).
{ name: 'search_ecosystem', description: 'Search across all verified AI ecosystem content. Returns results ranked by relevance. Leave ecosystem blank to search across all ecosystems. ' + EPISTEMIC_NOTICE, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, ecosystem: { type: 'string', description: 'Filter to a specific ecosystem slug. Omit to search all ecosystems', }, }, required: ['query'], }, }, - scripts/mcp-stdio.ts:62-69 (registration)Registration of search_ecosystem as an MCP tool in the STDIO transport server (scripts/mcp-stdio.ts). Uses TOOL_DEFINITIONS[3] for description and zod schema.
server.registerTool( 'search_ecosystem', { description: TOOL_DEFINITIONS[3].description, inputSchema: { query: z.string(), ecosystem: z.string().optional() }, }, (args) => handleToolCall('search_ecosystem', args as Record<string, unknown>, mockReq), ) - app/mcp/route.ts:53-60 (registration)Registration of search_ecosystem as an MCP tool in the HTTP transport server (app/mcp/route.ts). Uses TOOL_DEFINITIONS[3] for description and zod schema.
server.registerTool( 'search_ecosystem', { description: TOOL_DEFINITIONS[3].description, inputSchema: { query: z.string(), ecosystem: z.string().optional() }, }, (args) => handleToolCall('search_ecosystem', args as Record<string, unknown>, req), ) - lib/api-auth.ts:1-164 (helper)Supporting helper dependencies: checkEcosystemAccess (validates ecosystem slug and tier access), logApiRequest, and logQueryAudit are used within the search_ecosystem handler for access control and audit logging.
import { createHmac } from 'crypto' import type { NextRequest } from 'next/server' import type { SupabaseClient } from '@supabase/supabase-js' import { createServiceRoleClient } from './supabase-server' // ── Audit hash helper (H-6/M-1) ───────────────────────────────────────────── // Requires AUDIT_HASH_PEPPER to be set in env. Without it, hashes are HMAC // with an empty key — better than raw SHA-256 but lacks per-installation entropy. const AUDIT_HASH_PEPPER = process.env.AUDIT_HASH_PEPPER ?? '' if (!AUDIT_HASH_PEPPER) { console.warn('[api-auth] AUDIT_HASH_PEPPER not set — audit hashes lack per-installation pepper. Add to .env.local and Vercel env vars.') } function hashForAudit(value: string): string { return createHmac('sha256', AUDIT_HASH_PEPPER).update(value).digest('hex') } // ── Ecosystem slug validation (H-5) ───────────────────────────────────────── const SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/ // M-6: identical response for both not-found and tier-denied so callers cannot // enumerate which pro-only slugs exist. const ecosystemBlocked = () => Response.json({ error: 'Ecosystem not available', upgrade_url: '/dashboard' }, { status: 403 }) // ── Per-IP token bucket (M-7) ──────────────────────────────────────────────── // Per-process sliding window. In serverless / Fluid Compute each instance has // independent state — this bounds abuse within one instance. For global // enforcement across all instances add @upstash/ratelimit or a WAF rule. const IP_WINDOW_MS = 60_000 const IP_MAX_REQS = 200 const ipWindows = new Map<string, { n: number; reset: number }>() export function allowIp(ip: string): boolean { const now = Date.now() if (ipWindows.size > 10_000) { for (const [k, v] of ipWindows) { if (v.reset <= now) ipWindows.delete(k) } } const w = ipWindows.get(ip) if (!w || w.reset <= now) { ipWindows.set(ip, { n: 1, reset: now + IP_WINDOW_MS }) return true } if (w.n >= IP_MAX_REQS) return false w.n++ return true } // ── Query-param truncation (M-5) ───────────────────────────────────────────── function truncateQueryParams(params: Record<string, unknown>): Record<string, unknown> { return Object.fromEntries( Object.entries(params).map(([k, v]) => [ k, typeof v === 'string' && v.length > 256 ? v.slice(0, 256) + '…' : v, ]) ) } // ── Types ──────────────────────────────────────────────────────────────────── export type Tier = 'free' | 'pro' export type Profile = { id: string email: string api_key: string tier: Tier calls_used: number calls_reset_at: string | null stripe_customer_id: string | null stripe_subscription_id: string | null created_at: string } export type ServiceClient = SupabaseClient export const FREE_LIMIT = 100 export const PRO_LIMIT = 10_000 type AuthSuccess = { ok: true; profile: Profile; supabase: ServiceClient } type AuthFailure = { ok: false; response: Response } export type AuthResult = AuthSuccess | AuthFailure type ConsumeResult = { allowed: boolean profile_id: string | null tier: Tier | null calls_used: number was_reset: boolean } // ── Auth functions ──────────────────────────────────────────────────────────── // Atomically validates the API key, rolls the monthly window if elapsed, // enforces the tier limit, and increments the counter — all in a single // SECURITY DEFINER Postgres function under row-level locking. Returns either // the authenticated profile or a Response to short-circuit the route. export async function authenticateRequest(req: NextRequest): Promise<AuthResult> { // M-7: per-IP rate limit before hitting the DB const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? '' if (ip && !allowIp(ip)) { return { ok: false, response: Response.json({ error: 'Too many requests' }, { status: 429 }), } } const apiKey = req.headers.get('x-api-key') if (!apiKey) return invalidKey() return consumeApiCall(apiKey) } export async function consumeApiCall(apiKey: string): Promise<AuthResult> { const supabase = createServiceRoleClient() const { data, error } = await supabase .rpc('consume_api_call', { input_api_key: apiKey }) .maybeSingle<ConsumeResult>() if (error || !data) { return { ok: false, response: Response.json({ error: 'Service error' }, { status: 503 }), } } if (!data.profile_id) return invalidKey() if (!data.allowed) { return { ok: false, response: Response.json( { error: 'Monthly limit reached', tier: data.tier }, { status: 429 }, ), } } // Hydrate the rest of the profile fields the routes expect. const { data: profile, error: profileErr } = await supabase .from('profiles') .select('*') .eq('id', data.profile_id) .maybeSingle<Profile>() if (profileErr || !profile) { return { ok: false, response: Response.json({ error: 'Service error' }, { status: 503 }), } } // L-2: use the RPC's authoritative tier rather than the hydrated profile's. // The profile SELECT happens after the rate-limit check; a Stripe webhook // firing between the two calls could leave profile.tier stale. return { ok: true, profile: { ...profile, tier: data.tier as Tier }, supabase } } // Verifies the requested ecosystem exists and that the caller's tier is // permitted to use it. Free tier users are blocked from ecosystems where // available_on_free = false. export async function checkEcosystemAccess(