> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endorlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Endor Scores for Coding Agent Governance

> Understand the Endor Labs trust score for MCP servers and skills, including its scale, the dimensions it weighs, and the OWASP LLM Top 10 categories it covers.

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => <tr key={rowKey(row)}>
            {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

The Endor Score is the trust score Endor Labs publishes for two things in your Coding Agent Governance inventory:

* **Skills** discovered on developer machines at session start
* **MCP servers** your governed agents call

The score lets you compare risk across items at a glance and decide which to authorize, restrict, or block. This page describes how Endor Labs computes it and what it covers.

## Score scale

Every Endor Score is a number from `1` to `10` on a single dimension: **higher is safer**. A score of `1` means the evaluator found severe risk. A score of `10` means it found none.

For MCP servers, the headline score is the minimum across the four scored categories (**Supply Chain and Provenance**, **Authentication and Authorization**, **Operational Hygiene**, and **Input Validation and Injection**) that actually executed a check. A category with no applicable checks does not pull the score down.

### Bands and risk levels

For at-a-glance triage, the Coding Agent Governance overview and inventory show the numeric score.

Skills fall into four bands based on their score:

<YamlTable>
  {`
    - Band: **Unauthorized**
    Score: Below 4
    What it means: The evaluator considers the skill dangerous. Investigate before allowing it.
    - Band: **Caution**
    Score: 4 to below 8
    What it means: Findings exist that warrant review but do not on their own justify blocking.
    - Band: **Safe**
    Score: 8 or higher
    What it means: No significant findings.
    - Band: **Unscored**
    Score: None
    What it means: The scoring job has not evaluated the skill yet.
    `}
</YamlTable>

<Note>
  The **Skills** pie chart on the [Overview](/agent-governance/overview) page currently labels the **Caution** and **Safe** bands as **Suspicious** and **Approved**.
</Note>

The MCP Servers inventory also shows a categorical **risk band** derived from the numeric score, on the same cutoffs the skill bands use, so a low score reads as high risk.

<YamlTable>
  {`
    - Risk band: **High Risk**
    Score: Below 4
    What it means: The evaluator found severe risk.
    - Risk band: **Medium Risk**
    Score: 4 to below 8
    What it means: Findings warrant review but do not on their own justify blocking.
    - Risk band: **Low Risk**
    Score: 8 or higher
    What it means: No significant findings.
    - Risk band: **Unscored**
    Score: None
    What it means: The risk evaluator has not scored the server yet.
    `}
</YamlTable>

## How MCP servers are scored

The MCP evaluator runs a set of static checks against each server's configuration and published source across seven dimensions. The first four dimensions carry their own scores. The other three contribute findings and evidence without a per-dimension score.

<YamlTable>
  {`
    - Dimension: **Supply Chain and Provenance**
    Examples of what is checked: Package source verification, known CVEs, dependency pinning.
    - Dimension: **Authentication and Authorization**
    Examples of what is checked: Missing or weak authentication, secrets exposed in environment variables or source.
    - Dimension: **Operational Hygiene**
    Examples of what is checked: Spec deviations, repo signals, destructive tools, error noise.
    - Dimension: **Input Validation and Injection**
    Examples of what is checked: Prompt injection, command and SQL injection in tool arguments, unsafe deserialization.
    - Dimension: **Resource Access and Data Exposure**
    Examples of what is checked: Path traversal, cleartext handling of sensitive data.
    - Dimension: **Transport and Network Security**
    Examples of what is checked: TLS enforcement, certificate validation, public network binding.
    - Dimension: **LLM and Sampling Interaction**
    Examples of what is checked: Prompt injection risk in sampling requests the server sends to the model.
    `}
</YamlTable>

Each scored dimension reports its own `1`-`10` score and a short evidence string that lists the checks that fired in that category. Checks that need a live connection to the server never run, so dimensions score from source and configuration alone.

Endor Labs re-scores a server when its configuration changes, for example a new command, endpoint, transport, or environment variable name.

### OWASP LLM Top 10 coverage

The evaluator tags every MCP finding with the OWASP LLM Top 10 category it represents, so a low score traces back to a recognized risk class. The MCP evaluator covers these categories:

<YamlTable>
  {`
    - Category: **LLM01: Prompt Injection**
    Triggers it: Tool descriptions or inputs that allow instruction override.
    - Category: **LLM02: Sensitive Information Disclosure**
    Triggers it: Paths that read credentials, environment, or filesystem data.
    - Category: **LLM03: Supply Chain Vulnerabilities**
    Triggers it: Unverified package sources, known CVEs, missing dependency pinning.
    - Category: **LLM06: Excessive Agency**
    Triggers it: Command and code execution surfaces, destructive tools, overbroad permissions.
    - Category: **LLM07: System Prompt Leakage**
    Triggers it: Hardcoded secrets or static credentials in the server source.
    `}
</YamlTable>

## How skills are scored

An Endor Labs backend large-language-model workflow scores skills by reading the `SKILL.md` content the hook discovers. The workflow produces a `1`-`10` score on four dimensions.

<YamlTable>
  {`
    - Dimension: **Instruction Integrity**
    Examples of what is checked: Prompt injection, boundary violations, behavioral manipulation in the skill body.
    - Dimension: **Data Protection**
    Examples of what is checked: Credential access, file enumeration, data exfiltration.
    - Dimension: **Permission Boundaries**
    Examples of what is checked: Privilege claims, tool misuse, code execution outside the declared allowlist.
    - Dimension: **External Dependencies**
    Examples of what is checked: Untrusted source pulls, remote code loading, system modification.
    `}
</YamlTable>

The overall skill score is the minimum of the four dimension scores. Each dimension carries its own evidence string explaining the score. Scoring re-runs when the `SKILL.md` content hash changes, so Endor Labs re-evaluates an edit to a previously approved skill automatically.

The **Endor Score Factors** side panel on a skill's detail drawer shows the verdict: the overall score and risk band, the four dimension scores, and an expandable evidence row per dimension. In this example, a skill that presents as a harmless notes summarizer lands in **Caution** because its bundle carries scripts the evaluator cannot confirm are inert.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dPDQPDyDLWVGxIZu/images/agent-governance/skill-endor-score-factors.webp?fit=max&auto=format&n=dPDQPDyDLWVGxIZu&q=85&s=d7024b3dcaed65d646f531dca2dbed70" alt="Endor Score Factors side panel for a skill in the Caution band, showing a 5 out of 10 overall score and per-dimension scores" width="2400" height="1800" data-path="images/agent-governance/skill-endor-score-factors.webp" />

## What is not evaluated

The Endor Score is source-only. The evaluator never runs an MCP server or executes a skill to probe it. MCP scoring uses deterministic static checks. Skill scoring uses a large language model workflow, so it is not strictly deterministic.

* MCP servers are evaluated from configuration and published source. Live runtime probes are disabled in the Coding Agent Governance context.
* Skills are evaluated from `SKILL.md` content captured at session start. They are not exercised.
* Tool name collisions between MCP servers active in the same session, and periodic re-evaluation of MCP configuration drift, are not yet flagged.

## Where you see the score

You can see the score in the following locations:

* **[Skills inventory](/agent-governance/inventory#skills)**: The **Endor Score** column shows the score. Select a row, then select **Endor Score Factors** under **Risk** to open the per-dimension breakdown in a side panel.
* **[MCP Servers inventory](/agent-governance/inventory#mcp-servers)**: Select a server, then select **Endor Score Factors** under **Risk** to open the score and per-dimension findings in a side panel.
* **[Overview](/agent-governance/overview)**: The **Skills** pie chart groups loaded skills by band across the selected time range.

## Next steps

Continue with the following pages:

* See [Write a policy](/agent-governance/policies) to block or alert on items above a risk threshold.
* See [Review your agent inventory](/agent-governance/inventory) for the tables where the score appears.
