Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
queryYes
ecosystemNo

Implementation Reference

  • 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))
    }
  • 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'],
      },
    },
  • 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),
    )
  • 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(
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description discloses that results are 'ranked by relevance' and warns that 'Strata provides intelligence, not ground truth' with a recommendation to verify against source_urls. This is sufficient transparency for a search tool, though it does not mention pagination or rate limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the main purpose, followed by a usage tip and a crucial caveat. Every sentence provides value with no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (2 parameters, no output schema), the description is complete: it explains the search domain, how to use the optional parameter, and includes a caveat about verification. No critical information is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description adds meaning to both parameters: 'query' is implied by the search context, and 'ecosystem' is explicitly explained with the instruction to leave blank for cross-ecosystem search. This compensates for the lack of schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'Search across all verified AI ecosystem content' with a specific verb and resource, and distinguishes from sibling tools like list_ecosystems and get_latest_news by emphasizing broad search and relevance ranking.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides a usage hint ('Leave ecosystem blank to search across all ecosystems') but does not explicitly specify when to use this tool over alternatives like find_mcp_servers or get_best_practices. Usage is implied rather than stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PThrower/Strata'

If you have feedback or need assistance with the MCP directory API, please join our Discord server