> ## 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 renderSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" fill="currentColor" />
    </svg>;
  const renderFailureIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M11.9902 1.98633C12.5208 1.9864 13.0426 2.12743 13.501 2.39453C13.9581 2.66107 14.3372 3.04388 14.5986 3.50391L22.5967 17.5L22.6895 17.6738C22.8921 18.0851 22.9979 18.5387 22.9981 18.999C22.9981 19.5253 22.8605 20.0431 22.5977 20.499C22.3348 20.9549 21.9555 21.3332 21.5 21.5967C21.0445 21.8601 20.5272 21.9994 20.001 22H4.00001C3.47453 22.0031 2.95699 21.868 2.50001 21.6084C2.04032 21.3471 1.65712 20.9684 1.39063 20.5117C1.12431 20.055 0.983643 19.5355 0.982429 19.0068C0.981281 18.4793 1.11967 17.9611 1.38282 17.5039L9.38184 3.50391C9.64344 3.04359 10.023 2.66111 10.4805 2.39453C10.9388 2.12755 11.4598 1.98636 11.9902 1.98633ZM12 16.9004C11.3925 16.9004 10.9004 17.3925 10.9004 18C10.9004 18.6075 11.3925 19.0996 12 19.0996H12.0098C12.6173 19.0996 13.1104 18.6075 13.1104 18C13.1104 17.3925 12.6173 16.9004 12.0098 16.9004H12ZM12 5.90039C11.3925 5.9004 10.9004 6.39249 10.9004 7V13C10.9004 13.6075 11.3925 14.0996 12 14.0996C12.6075 14.0996 13.0996 13.6075 13.0996 13V7C13.0996 6.39249 12.6075 5.90039 12 5.90039Z" fill="currentColor" />
    </svg>;
  const renderPartialSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 5C13.8565 5 15.6374 5.73705 16.9502 7.0498C18.263 8.36256 19 10.1435 19 12C19 13.8565 18.263 15.6374 16.9502 16.9502C15.6374 18.263 13.8565 19 12 19C11.4477 19 11 18.5523 11 18V6C11 5.73478 11.1054 5.4805 11.293 5.29297C11.4805 5.10543 11.7348 5 12 5Z" fill="currentColor" />
    </svg>;
  const renderPendingIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
      <path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
      <path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
      <path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
      <path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
      <path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
      <path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
      <path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
    </svg>;
  const renderRunningIcon = () => <svg className="yt-status-running-svg" viewBox="22 22 44 44" width="100%" height="100%" aria-hidden="true">
      <circle className="yt-status-running-circle" cx="44" cy="44" r="20.2" fill="none" stroke="currentColor" strokeWidth="3.6" />
    </svg>;
  const renderSkippedIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="12" cy="12" r="10" />
      <path d="M8 12h8" />
    </svg>;
  const STATUS_ICON_DEFS = [{
    re: /^-status-success-$/i,
    colorClass: 'yt-status-color-success',
    label: 'Success',
    render: renderSuccessIcon
  }, {
    re: /^-status-failure-$/i,
    colorClass: 'yt-status-color-failure',
    label: 'Failure',
    render: renderFailureIcon
  }, {
    re: /^-status-partial-success-$/i,
    colorClass: 'yt-status-color-partial',
    label: 'Partial success',
    render: renderPartialSuccessIcon
  }, {
    re: /^-status-pending-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Pending',
    render: renderPendingIcon
  }, {
    re: /^-status-running-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Running',
    render: renderRunningIcon
  }, {
    re: /^-status-skipped-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Skipped',
    render: renderSkippedIcon
  }];
  const renderStatusBadge = def => <span className={['yt-status-icon', def.colorClass].join(' ')} role="img" aria-label={def.label} title={def.label}>
      {def.render()}
    </span>;
  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>;
    const statusBadge = STATUS_ICON_DEFS.find(def => def.re.test(text));
    if (statusBadge) return renderStatusBadge(statusBadge);
    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);
  const scrollWrapRef = useRef(null);
  const [isScrollable, setIsScrollable] = 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]);
  useEffect(() => {
    const wrap = scrollWrapRef.current;
    if (!wrap) return undefined;
    const updateScrollable = () => {
      setIsScrollable(wrap.scrollWidth > wrap.clientWidth);
    };
    updateScrollable();
    const observer = new ResizeObserver(updateScrollable);
    observer.observe(wrap);
    return () => observer.disconnect();
  }, [mounted, data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <div ref={scrollWrapRef} style={{
    overflowX: 'auto',
    margin: '1.75rem 0'
  }} role={isScrollable ? 'region' : undefined} aria-label={isScrollable ? 'Scrollable table' : undefined} tabIndex={isScrollable ? 0 : undefined}>
      <table style={{
    display: 'table',
    width: '100%',
    minWidth: '100%',
    margin: 0
  }}>
        <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>
    </div>;
};

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 runs 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 that actually executed a check. The categories are **Supply Chain and Provenance**, **Authentication and Authorization**, **Operational Hygiene**, and **Input Validation and Injection**. 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. 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. It lists 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.