systemprompt-mcp 0.43.0

Native Model Context Protocol (MCP) implementation for systemprompt.io. Orchestration, per-server OAuth2, RBAC middleware, and tool-call governance — the core of the AI governance pipeline.
Documentation
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Artifact Viewer</title>
  <style>
    /* The shell is the chrome around the mounted artifact, so it has to follow
     * the viewer's scheme for the same reason the artifact does: hardcoded
     * light here put a white band around a dark artifact. It cannot use the
     * --mcpui-* tokens — those live in the nested document — so it restates
     * the few values it needs with light-dark(). */
    :root {
      color-scheme: light dark;
      --shell-bg:      light-dark(oklch(1 0 0),        oklch(0.20 0.01 50));
      --shell-ink:     light-dark(oklch(0.20 0.01 50), oklch(0.98 0.004 70));
      --shell-ink-dim: light-dark(oklch(0.41 0.01 60), oklch(0.86 0.010 60));
      --shell-border:  light-dark(oklch(0.92 0.008 65), oklch(0.34 0.01 55));
      --shell-sunken:  light-dark(oklch(0.97 0.005 70), oklch(0.27 0.01 50));
      --shell-accent:  light-dark(oklch(0.67 0.18 50), oklch(0.72 0.17 52));
      --shell-danger:  light-dark(oklch(0.63 0.21 25), oklch(0.70 0.17 20));
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    html, body { height: 100%; }
    body {
      font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      font-size: 14px;
      color: var(--shell-ink);
      background: var(--shell-bg);
    }
    #frame {
      display: block;
      width: 100%;
      border: 0;
      min-height: 120px;
    }
    .status {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 10px;
      padding: 32px;
      color: var(--shell-ink-dim);
    }
    .spinner {
      width: 18px;
      height: 18px;
      border: 2px solid var(--shell-border);
      border-top-color: var(--shell-accent);
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
    }
    @keyframes spin { to { transform: rotate(360deg); } }
    /* The spinner is the only motion here, and a spinner that cannot spin is
     * better than one that ignores the preference. */
    @media (prefers-reduced-motion: reduce) {
      .spinner { animation-duration: 2.4s; }
    }
    .fallback { padding: 16px; }
    .fallback pre {
      background: var(--shell-sunken);
      border: 1px solid var(--shell-border);
      border-radius: 6px;
      padding: 12px;
      overflow-x: auto;
      font-size: 12px;
      color: var(--shell-ink);
    }
    .error {
      margin: 16px;
      padding: 12px;
      border: 1px solid color-mix(in oklab, var(--shell-danger) 45%, transparent);
      border-left: 3px solid var(--shell-danger);
      border-radius: 6px;
      background: color-mix(in oklab, var(--shell-danger) 12%, transparent);
      color: var(--shell-ink);
    }
  </style>
</head>
<body>
  <div id="root">
    <div class="status"><span class="spinner"></span><span>Loading artifact…</span></div>
  </div>
  <script>
    /*MCP_UI_CONSTANTS*/

    // Method names come from the generated MCP_UI constants; never spell them
    // out here. All artifact markup is rendered server-side, so this shell
    // needs no knowledge of artifact types.
    const HTML_MIME_PREFIX = 'text/html';
    const PENDING = new Map();
    let requestId = 0;
    let lastSize = { width: 0, height: 0 };

    function post(message) {
      window.parent.postMessage(message, '*');
    }

    function request(method, params) {
      return new Promise((resolve, reject) => {
        const id = ++requestId;
        PENDING.set(id, { resolve, reject });
        post({ jsonrpc: '2.0', id, method, params });
      });
    }

    function publishSize() {
      const doc = document.documentElement;
      const width = Math.ceil(Math.max(doc.scrollWidth, document.body.scrollWidth));
      const height = Math.ceil(Math.max(doc.scrollHeight, document.body.scrollHeight));
      if (!height || (width === lastSize.width && height === lastSize.height)) {
        return;
      }
      lastSize = { width, height };
      post({
        jsonrpc: '2.0',
        method: MCP_UI.SIZE_CHANGED,
        params: { width, height }
      });
    }

    // Host theme (SEP-1865 `hostContext`). Without this the shell and the
    // artifact both fall back to `color-scheme: light dark`, which resolves off
    // the VIEWER'S OS — so a light host on a dark-OS machine showed a dark
    // artifact in a light pane. The host is the only party that knows its own
    // theme, so it has to say; when it does not, the OS fallback still applies
    // and nothing changes.
    let hostTheme = null;
    let mountedHtml = null;

    function applyHostContext(context) {
        const theme = context && context.theme;
        if (theme !== 'light' && theme !== 'dark') {
            return;
        }
        if (theme === hostTheme) {
            return;
        }
        hostTheme = theme;
        // The shell's own chrome — the loading state, the error panel, the
        // band around the artifact.
        document.documentElement.style.colorScheme = theme;
        publishTheme();
    }

    // The artifact is a sandboxed srcdoc iframe with no allow-same-origin, so
    // its DOM is unreachable from here; the theme has to be relayed as a
    // message and stamped by frame.js on the other side.
    function publishTheme() {
        const frame = document.getElementById('frame');
        if (!hostTheme || !frame || !frame.contentWindow) {
            return;
        }
        frame.contentWindow.postMessage({
            jsonrpc: '2.0',
            method: MCP_UI.HOST_CONTEXT_CHANGED,
            params: { hostContext: { theme: hostTheme } }
        }, '*');
    }

    // The tool result carries the rendered artifact as an embedded ui://
    // resource — the zero-round-trip path.
    function embeddedHtml(result) {
      for (const block of (result && result.content) || []) {
        const resource = block && block.resource;
        const mime = resource && (resource.mimeType || resource.mime_type);
        if (resource && typeof resource.text === 'string' && mime && mime.startsWith(HTML_MIME_PREFIX)) {
          return resource.text;
        }
      }
      return null;
    }

    // Spec-sanctioned fallback: read the artifact's own ui:// resource from
    // the server when the host does not forward embedded content blocks.
    async function fetchedHtml(result) {
      const meta = result && result._meta;
      const uri = meta && meta['io.systemprompt/ui-resource-uri'];
      if (!uri) {
        return null;
      }
      const read = await request('resources/read', { uri });
      for (const contents of (read && read.contents) || []) {
        if (typeof contents.text === 'string') {
          return contents.text;
        }
      }
      return null;
    }

    function mount(html) {
      mountedHtml = html;
      const root = document.getElementById('root');
      root.innerHTML = '<iframe id="frame" sandbox="allow-scripts allow-popups"></iframe>';
      const frame = document.getElementById('frame');
      // Why on load rather than immediately: srcdoc parses asynchronously, so
      // frame.js is not listening yet when mount() returns. A theme that
      // arrived before the artifact did is replayed here.
      frame.addEventListener('load', publishTheme);
      frame.srcdoc = html;
    }

    function showFallback(result) {
      const payload = (result && result.structuredContent) || result || {};
      const wrapper = document.createElement('div');
      wrapper.className = 'fallback';
      const pre = document.createElement('pre');
      pre.textContent = JSON.stringify(payload, null, 2);
      wrapper.appendChild(pre);
      const root = document.getElementById('root');
      root.innerHTML = '';
      root.appendChild(wrapper);
      publishSize();
    }

    function showError(message) {
      const div = document.createElement('div');
      div.className = 'error';
      div.textContent = message;
      const root = document.getElementById('root');
      root.innerHTML = '';
      root.appendChild(div);
      publishSize();
    }

    async function render(result) {
      const inline = embeddedHtml(result);
      if (inline) {
        mount(inline);
        return;
      }
      try {
        const fetched = await fetchedHtml(result);
        if (fetched) {
          mount(fetched);
          return;
        }
      } catch (err) {
        console.error('resources/read for artifact failed:', err);
      }
      showFallback(result);
    }

    window.addEventListener('message', (event) => {
      const data = event.data || {};
      try {
        if (data.id && PENDING.has(data.id)) {
          const { resolve, reject } = PENDING.get(data.id);
          PENDING.delete(data.id);
          if (data.error) {
            reject(new Error(data.error.message || 'Request failed'));
          } else {
            resolve(data.result);
          }
          return;
        }

        // The mounted artifact reports its own size; grow to fit and pass the
        // measurement up to the host.
        if (data.method === MCP_UI.SIZE_CHANGED && data.params) {
          const frame = document.getElementById('frame');
          if (frame && event.source === frame.contentWindow) {
            frame.style.height = data.params.height + 'px';
            publishSize();
          }
          return;
        }

        // Only the host may set the theme; the mounted artifact must not be
        // able to repaint the shell around it.
        if (data.method === MCP_UI.HOST_CONTEXT_CHANGED && event.source === window.parent) {
          applyHostContext((data.params && data.params.hostContext) || data.params);
          return;
        }

        if (data.method === MCP_UI.TOOL_RESULT) {
          render(data.params);
        }
      } catch (err) {
        console.error('Artifact shell failed to render:', err);
        showError(err.message || 'Failed to render artifact');
      }
    });

    request(MCP_UI.INITIALIZE, {
      appInfo: { name: 'systemprompt-artifact-viewer', version: '1.0.0' },
      appCapabilities: { hostContext: {} },
      protocolVersion: MCP_UI.PROTOCOL_VERSION
    }).then((result) => {
      // A host that reports its theme up front saves a repaint; one that only
      // sends the notification later is handled by the listener above.
      applyHostContext(result && result.hostContext);
      post({ jsonrpc: '2.0', method: MCP_UI.INITIALIZED, params: {} });
      publishSize();
    }).catch((err) => {
      console.error('MCP Apps initialize failed:', err);
    });
  </script>
</body>
</html>