Skip to main content
Glama

find_equivalent

Read-onlyIdempotent

Find the same medical concept across ICD-11, SNOMED CT, LOINC, RxNorm, and MeSH. Supports terminology mapping and data integration by comparing how terminologies represent a term.

Instructions

Search for equivalent terms across multiple medical terminologies.

Use this tool to:

  • Find the same concept in different coding systems

  • Compare how terminologies represent a concept

  • Support terminology mapping and data integration

Searches across: ICD-11, SNOMED CT, LOINC, RxNorm, and MeSH. Set target_terminologies to limit which are searched, or set source_terminology to exclude one (e.g. when you already have a code from that terminology and want equivalents elsewhere). The two combine: source is subtracted from targets.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
termYesMedical term to search (e.g., "diabetes", "aspirin")
source_terminologyNoIf set, this terminology is excluded from the search. Use this when the term came from this terminology and you want equivalents in the others. Combines with target_terminologies by subtraction (source is removed from the target list).
target_terminologiesNoLimit the search to these terminologies. If omitted, all five are searched.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
termYes
source_terminologyYes
searched_terminologiesYes
resultsYes

Implementation Reference

  • The main handler function for the find_equivalent tool. Parses params, computes target terminologies, searches up to 5 terminologies in parallel (ICD-11, SNOMED CT, LOINC, RxNorm, MeSH), builds markdown and structured output.
    async function handleFindEquivalent(args: Record<string, unknown>): Promise<CallToolResult> {
      try {
        const params = FindEquivalentParamsSchema.parse(args);
        const term = params.term;
        const requestedTargets = params.target_terminologies ?? [...ALL_TERMINOLOGIES];
        const targets: TerminologyKey[] = (params.source_terminology
          ? requestedTargets.filter((t) => t !== params.source_terminology)
          : requestedTargets) as TerminologyKey[];
    
        if (targets.length === 0) {
          const requested = params.target_terminologies
            ? `target_terminologies=${JSON.stringify(params.target_terminologies)}`
            : 'all terminologies';
          const empty: FindEquivalentOutput = {
            term,
            source_terminology: params.source_terminology ?? null,
            searched_terminologies: [],
            results: {},
          };
          return {
            content: [{
              type: 'text',
              text: `# Cross-Terminology Search: "${term}"\n\nNo terminologies left to search after excluding source_terminology="${params.source_terminology}" from ${requested}. Widen target_terminologies or drop source_terminology.`,
            }],
            structuredContent: empty,
          };
        }
    
        // Build a single typed map keyed by enum key. Markdown is derived from
        // this same map below — no duplicated data.
        const entries: Partial<Record<TerminologyKey, FindEquivalentEntry>> = {};
        const searches: Promise<void>[] = [];
    
        const ok = (items: FindEquivalentEntry['items']): FindEquivalentEntry => ({
          found: items.length > 0,
          error: null,
          items,
        });
        const fail = (error: string): FindEquivalentEntry => ({ found: false, error, items: [] });
    
        if (targets.includes('icd11')) {
          searches.push(
            (async () => {
              try {
                const client = getWHOClient();
                const response = await client.search(term, 'en', 5);
                const icdResults = response.destinationEntities ?? [];
                entries.icd11 = ok(
                  icdResults.slice(0, 5).map((r) => ({
                    code: r.theCode ?? 'N/A',
                    title: r.title ?? 'N/A',
                  })),
                );
              } catch (e) {
                entries.icd11 = fail(e instanceof Error ? e.message : 'Error');
              }
            })(),
          );
        }
    
        if (targets.includes('snomed')) {
          if (!SNOMED_TOOLS_ENABLED) {
            entries.snomed = fail(SNOMED_DISABLED_NOTE);
          } else {
            searches.push(
              (async () => {
                try {
                  const client = getSNOMEDClient();
                  const snomedResults = await client.searchConcepts(term, true, 5);
                  entries.snomed = ok(
                    snomedResults.map((r) => ({ code: r.conceptId, title: r.pt })),
                  );
                } catch (e) {
                  const errMsg = e instanceof Error ? e.message : 'Error';
                  entries.snomed = fail(errMsg.includes('ETIMEDOUT') ? 'Server unavailable' : errMsg);
                }
              })(),
            );
          }
        }
    
        if (targets.includes('loinc')) {
          searches.push(
            (async () => {
              try {
                const client = getNLMClient();
                const loincResponse = await client.searchLOINC(term, 5);
                const loincResults = loincResponse.items ?? [];
                entries.loinc = ok(
                  loincResults.map((r) => ({ code: r.LOINC_NUM, title: r.LONG_COMMON_NAME })),
                );
              } catch (e) {
                entries.loinc = fail(e instanceof Error ? e.message : 'Error');
              }
            })(),
          );
        }
    
        if (targets.includes('rxnorm')) {
          searches.push(
            (async () => {
              try {
                const client = getRxNormClient();
                const rxResults = await client.searchDrugs(term);
                entries.rxnorm = ok(
                  rxResults.drugs.slice(0, 5).map((r) => ({ code: r.rxcui, title: r.name })),
                );
              } catch (e) {
                entries.rxnorm = fail(e instanceof Error ? e.message : 'Error');
              }
            })(),
          );
        }
    
        if (targets.includes('mesh')) {
          searches.push(
            (async () => {
              try {
                const client = getMeSHClient();
                const meshResults = await client.searchDescriptors(term, 'contains', 5);
                entries.mesh = ok(
                  meshResults.map((r) => ({ code: r.id, title: r.label })),
                );
              } catch (e) {
                entries.mesh = fail(e instanceof Error ? e.message : 'Error');
              }
            })(),
          );
        }
    
        await Promise.all(searches);
    
        // Markdown derived from the same entries map, in target order so output
        // is stable regardless of which API resolved first.
        const lines: string[] = [];
        lines.push(`# Cross-Terminology Search: "${term}"`);
        if (params.source_terminology) {
          lines.push(`_Excluding source_terminology=\`${params.source_terminology}\` from the search._`);
        }
        lines.push('');
    
        for (const key of targets) {
          const entry = entries[key];
          if (!entry) continue;
          lines.push(`## ${TERMINOLOGY_LABELS[key]}`);
          lines.push('');
          if (entry.error) {
            lines.push(`⚠️ ${entry.error}`);
          } else if (!entry.found) {
            lines.push('No matches found.');
          } else {
            for (const item of entry.items) {
              lines.push(`- ${item.code} - ${item.title}`);
            }
          }
          lines.push('');
        }
    
        const foundIn = targets
          .filter((k) => entries[k]?.found)
          .map((k) => TERMINOLOGY_LABELS[k]);
    
        lines.push('---');
        lines.push('');
        if (foundIn.length > 0) {
          lines.push(`**Found in:** ${foundIn.join(', ')}`);
        } else {
          lines.push('**No matches found in any terminology.**');
        }
    
        if (targets.includes('snomed') && SNOMED_TOOLS_ENABLED) {
          lines.push('');
          lines.push(SNOMED_DISCLAIMER);
        }
    
        const structured: FindEquivalentOutput = {
          term,
          source_terminology: params.source_terminology ?? null,
          searched_terminologies: targets,
          results: entries,
        };
    
        return {
          content: [{ type: 'text', text: lines.join('\n') }],
          structuredContent: structured,
        };
      } catch (error) {
        return handleToolError(error);
      }
    }
  • Input and output Zod schemas for the find_equivalent tool. Input takes term, optional source_terminology (excluded), and optional target_terminologies (limit search). Output has normalized {code, title} items per terminology.
    export const FindEquivalentParamsSchema = z.object({
      term: z.string().min(1).describe('Medical term to search (e.g., "diabetes", "aspirin")'),
      source_terminology: TerminologyEnum
        .optional()
        .describe(
          'If set, this terminology is excluded from the search. Use this when the term came from this terminology and you want equivalents in the others. Combines with target_terminologies by subtraction (source is removed from the target list).',
        ),
      target_terminologies: z
        .array(TerminologyEnum)
        .optional()
        .describe('Limit the search to these terminologies. If omitted, all five are searched.'),
    });
    
    // ============================================================================
    // find_equivalent output schema (structuredContent)
    //
    // Shape note: per-terminology items are normalized to { code, title } so a
    // consumer can iterate uniformly across results. The native identifier
    // shape (LOINC_NUM vs conceptId vs rxcui vs MeSH ID) is collapsed into the
    // generic "code" field. If an LLM/client needs the native shape, it should
    // call the per-terminology search tool directly with the relevant code.
    // ============================================================================
    
    const FindEquivalentItemSchema = z.object({
      code: z.string(),
      title: z.string(),
    });
    
    const FindEquivalentTerminologyResultSchema = z.object({
      found: z.boolean(),
      // Populated when the upstream call failed (timeout, server error, or — for
      // SNOMED — when the SNOMED tools are disabled in this server).
      error: z.string().nullable(),
      items: z.array(FindEquivalentItemSchema),
    });
    
    export const FindEquivalentOutputSchema = z.object({
      term: z.string(),
      source_terminology: TerminologyEnum.nullable(),
      searched_terminologies: z.array(TerminologyEnum),
      // Each terminology key is present only if it was actually searched (i.e.
      // it's in searched_terminologies). Absent keys mean "not requested",
      // empty items+found:false means "searched, no hits".
      results: z.object({
        icd11: FindEquivalentTerminologyResultSchema.optional(),
        snomed: FindEquivalentTerminologyResultSchema.optional(),
        loinc: FindEquivalentTerminologyResultSchema.optional(),
        rxnorm: FindEquivalentTerminologyResultSchema.optional(),
        mesh: FindEquivalentTerminologyResultSchema.optional(),
      }),
    });
    
    export type FindEquivalentOutput = z.infer<typeof FindEquivalentOutputSchema>;
  • Registration of the find_equivalent tool with the tool registry, linking the tool definition to its handler function.
    toolRegistry.register(findEquivalentTool, handleFindEquivalent);
  • Tool definition object for find_equivalent, including name, description, input/output schemas, and read-only annotations.
    const findEquivalentTool: Tool = {
      name: 'find_equivalent',
      description: `Search for equivalent terms across multiple medical terminologies.
    
    Use this tool to:
    - Find the same concept in different coding systems
    - Compare how terminologies represent a concept
    - Support terminology mapping and data integration
    
    Searches across: ICD-11, SNOMED CT, LOINC, RxNorm, and MeSH. Set \`target_terminologies\` to limit which are searched, or set \`source_terminology\` to exclude one (e.g. when you already have a code from that terminology and want equivalents elsewhere). The two combine: source is subtracted from targets.`,
      inputSchema: buildInputSchema(FindEquivalentParamsSchema),
      outputSchema: buildOutputSchema(FindEquivalentOutputSchema),
      annotations: READ_ONLY_TOOL_ANNOTATIONS,
    };
Behavior5/5

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

Annotations already indicate read-only, idempotent, open-world. Description adds substantive behavioral context: lists the five terminologies searched, explains the subtractive interaction between source and target parameters, and gives a concrete example of excluding a source terminology. No contradictions.

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?

Description is concise: one-line purpose statement followed by bullet points for use cases and a brief parameter explanation. Every sentence is informative, front-loaded with the core action.

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?

With three parameters, an output schema, and clear annotations, the description fully covers purpose, usage scenarios, parameter interactions, and scope. No gaps remain for agent decision-making.

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

Parameters5/5

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

Schema description coverage is 100%, but the tool description adds significant value by explaining how source_terminology and target_terminologies combine (subtraction), which is not evident from the schema alone. This enhances parameter understanding.

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?

Description clearly states 'Search for equivalent terms across multiple medical terminologies' with a specific verb and resource. It distinguishes from sibling lookup tools by emphasizing cross-terminology equivalence mapping.

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

Usage Guidelines5/5

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

Explicitly lists use cases (find same concept, compare representations, support mapping/integration). Explains parameter logic for target_terminologies and source_terminology, including their interaction by subtraction, providing clear when-to-use and how-to-use guidance.

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/SidneyBissoli/medical-terminologies-mcp'

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