<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abe Wheeler</title>
    <description>The latest articles on DEV Community by Abe Wheeler (@abewheeler).</description>
    <link>https://dev.to/abewheeler</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3648565%2Fd0908335-f89f-46af-9aeb-c5ea99da2236.png</url>
      <title>DEV Community: Abe Wheeler</title>
      <link>https://dev.to/abewheeler</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abewheeler"/>
    <language>en</language>
    <item>
      <title>Shared, hosted memory</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Thu, 30 Jul 2026 19:49:34 +0000</pubDate>
      <link>https://dev.to/abewheeler/shared-hosted-memory-3job</link>
      <guid>https://dev.to/abewheeler/shared-hosted-memory-3job</guid>
      <description>&lt;p&gt;We just launched shared and hosted Memory in Alignbase.&lt;/p&gt;

&lt;p&gt;Alignbase is now your headless, model-agnostic place to govern and coordinate:&lt;br&gt;
    🤖 Agents&lt;br&gt;
    🗂️ &lt;code&gt;AGENTS.md&lt;/code&gt;s&lt;br&gt;
    🔧 Skills&lt;br&gt;
    + 🧠 Memory!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg45c22rs8tp4m9f1pf28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg45c22rs8tp4m9f1pf28.png" alt="Alignbase memory page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI agents often start each session without the working context they built last time. Alignbase now gives each agent one hosted Memory file it can read every session and update as it works.&lt;/p&gt;

&lt;p&gt;Teams can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Automatically create a writable Memory for every new agent&lt;/li&gt;
&lt;li&gt;Share one Memory file across any number of agents&lt;/li&gt;
&lt;li&gt;Turn write access off per agent while keeping Memory readable&lt;/li&gt;
&lt;li&gt;Track version history and agent activity in one place&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Agents manage their own Memory, so users do not have to review every update. Memory holds current tasks, decisions, learned preferences, and known traps.&lt;/p&gt;

&lt;p&gt;It stays separate from AGENTS.md instructions, Skills, and full knowledge bases.&lt;/p&gt;

&lt;p&gt;Agents now pick up where any other agent left off!&lt;/p&gt;

&lt;p&gt;See it at &lt;a href="https://alignbase.ai" rel="noopener noreferrer"&gt;https://alignbase.ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>mcp</category>
    </item>
    <item>
      <title>I Built a Git-Native SOC 2 Tool Because I Didn’t Want Another SaaS Bill</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:30:29 +0000</pubDate>
      <link>https://dev.to/abewheeler/i-built-a-git-native-soc-2-tool-because-i-didnt-want-another-saas-bill-dgf</link>
      <guid>https://dev.to/abewheeler/i-built-a-git-native-soc-2-tool-because-i-didnt-want-another-saas-bill-dgf</guid>
      <description>&lt;p&gt;I recently &lt;a href="https://x.com/AbeWheeler5/status/2081768596284256663?s=20" rel="noopener noreferrer"&gt;posted this on X&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Didn’t feel like paying a zillion dollars again for basic SOC 2 software, so I rolled my own this weekend.&lt;/p&gt;

&lt;p&gt;100% file-based, uses Git for audit trails. Agents can absolutely cook on this thing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That post got far more attention than I expected, so I thought developers might want a closer look at the idea and how it works.&lt;/p&gt;

&lt;p&gt;The project is called &lt;a href="https://github.com/Sunpeak-AI/filegrc" rel="noopener noreferrer"&gt;filegrc&lt;/a&gt;. It is an open source, Git-native system for managing SOC 2 records and audit evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to solve
&lt;/h2&gt;

&lt;p&gt;A lot of SOC 2 work is structured data wrapped in a web application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Policies and their approval state&lt;/li&gt;
&lt;li&gt;Controls, owners, and implementation details&lt;/li&gt;
&lt;li&gt;Recurring security work&lt;/li&gt;
&lt;li&gt;Risks, vendors, systems, and incidents&lt;/li&gt;
&lt;li&gt;Evidence collected during an audit period&lt;/li&gt;
&lt;li&gt;Links between all those records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Engineering teams already have a good system for reviewing structured text, tracking changes, assigning authorship, and preserving history.&lt;/p&gt;

&lt;p&gt;That's Git.&lt;/p&gt;

&lt;p&gt;I wanted to see what SOC 2 work would look like if the repository were the system of record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic architecture
&lt;/h2&gt;

&lt;p&gt;filegrc stores structured records as JSON and long-form content as Markdown.&lt;/p&gt;

&lt;p&gt;A workspace looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;company-grc/
├── AGENTS.md
├── data/
│   ├── policies/
│   ├── systems/
│   ├── risks/
│   ├── obligations/
│   ├── evidence/
│   ├── audits/
│   └── workspace.json
├── package.json
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A policy record might contain fields such as:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy-information-security"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information Security Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ownerIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"person-policy-owner"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approverIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"person-independent-approver"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"effectiveOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewCadence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"calendar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"year"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"interval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"anchorDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-01"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The policy text lives beside it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/policies/policy-information-security.json
data/policies/policy-information-security.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The engine keeps data structured when it needs to validate, filter, calculate deadlines, or connect records. Policies, procedures, meeting notes, audit responses, and other long-form work stay in Markdown.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Git works well here
&lt;/h2&gt;

&lt;p&gt;Git already records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed&lt;/li&gt;
&lt;li&gt;Who changed it&lt;/li&gt;
&lt;li&gt;When the change entered the repository&lt;/li&gt;
&lt;li&gt;The exact diff&lt;/li&gt;
&lt;li&gt;The revision containing the change&lt;/li&gt;
&lt;li&gt;The review that approved it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That removes the need for a second application-level change log.&lt;/p&gt;

&lt;p&gt;Domain dates still need to remain explicit. A control test performed on July 10 needs a &lt;code&gt;completedOn&lt;/code&gt; date, even if someone commits the record on July 11. Git records the repository change. The record describes what happened in the security program.&lt;/p&gt;

&lt;p&gt;Keeping those concepts separate makes the history easier to inspect.&lt;/p&gt;

&lt;p&gt;It also makes normal engineering workflows available:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff
git blame data/policies/policy-information-security.md
git log &lt;span class="nt"&gt;--&lt;/span&gt; data/controls/control-access-review.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A reviewer can examine a policy change using the same tools they use for code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The files still need a domain model
&lt;/h2&gt;

&lt;p&gt;Putting JSON in Git is the easy part. The harder part is enforcing the relationships and lifecycle rules that make the records useful.&lt;/p&gt;

&lt;p&gt;filegrc has a versioned model registry that defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource types&lt;/li&gt;
&lt;li&gt;Required fields&lt;/li&gt;
&lt;li&gt;Valid values&lt;/li&gt;
&lt;li&gt;Relationships&lt;/li&gt;
&lt;li&gt;Conditional requirements&lt;/li&gt;
&lt;li&gt;Markdown content slots&lt;/li&gt;
&lt;li&gt;Default UI metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The validator, CLI, browser forms, search index, and generated documentation all consume that model.&lt;/p&gt;

&lt;p&gt;This means the browser and CLI do not have separate ideas about what a valid control or evidence record looks like.&lt;/p&gt;

&lt;p&gt;You can validate an entire workspace with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The validator checks individual records as well as relationships between them.&lt;/p&gt;

&lt;p&gt;For example, it can detect missing owners, broken references, invalid date ranges, incomplete policy events, missing Markdown, and evidence attachments that resolve outside the workspace.&lt;/p&gt;
&lt;h2&gt;
  
  
  Headless use is part of the product
&lt;/h2&gt;

&lt;p&gt;I wanted coding agents to work with filegrc without learning the whole model from scratch.&lt;/p&gt;

&lt;p&gt;A generated workspace includes agent instructions, and the CLI exposes model-driven guidance:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx filegrc guide risk-assessment &lt;span class="nt"&gt;--json&lt;/span&gt;
npx filegrc list risk &lt;span class="nt"&gt;--json&lt;/span&gt;
npx filegrc references system-id &lt;span class="nt"&gt;--json&lt;/span&gt;
npx filegrc scaffold risk-assessment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"2026 Annual Risk Assessment"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;An agent can ask which fields a record requires, inspect valid relationship targets, scaffold a mutation, and run validation afterward.&lt;/p&gt;

&lt;p&gt;Recurring and event-driven work is available through the CLI too:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx filegrc obligations &lt;span class="nt"&gt;--json&lt;/span&gt;

npx filegrc trigger person-started &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--occurred-on&lt;/span&gt; 2026-07-25 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; person-id

npx filegrc complete-action action-item-id &lt;span class="se"&gt;\&lt;/span&gt;
  completion-record.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--completed-on&lt;/span&gt; 2026-07-25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The browser and CLI call the same domain functions, so an agent receives the same validation and calculations as a person using the UI.&lt;/p&gt;

&lt;p&gt;That is what I meant when I said agents can cook on this thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  A zero-dependency Node.js engine
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhpfgimov6yz4vvhqu36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhpfgimov6yz4vvhqu36.png" alt="filegrc browser experience" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;filegrc&lt;/code&gt; engine uses Node.js built-in modules only.&lt;/p&gt;

&lt;p&gt;It does not require a database, compilation step, bundler, or hosted service. It runs locally, in CI, or in a basic server environment with Node.js 20 or newer and Git.&lt;/p&gt;

&lt;p&gt;The engine handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace validation&lt;/li&gt;
&lt;li&gt;CRUD operations&lt;/li&gt;
&lt;li&gt;Search&lt;/li&gt;
&lt;li&gt;Recurring obligations&lt;/li&gt;
&lt;li&gt;Policy-triggered events&lt;/li&gt;
&lt;li&gt;Program and audit readiness checks&lt;/li&gt;
&lt;li&gt;Evidence packet generation&lt;/li&gt;
&lt;li&gt;A local web interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writes are atomic, paths are checked against the workspace boundary, and CRUD commands do not create Git commits unless the user explicitly asks for one.&lt;/p&gt;
&lt;h2&gt;
  
  
  What filegrc does not replace
&lt;/h2&gt;

&lt;p&gt;filegrc manages GRC records and audit evidence. It does not replace infrastructure logging, identity management, backups, endpoint protection, monitoring, training, or incident detection.&lt;/p&gt;

&lt;p&gt;Those systems remain the authoritative sources.&lt;/p&gt;

&lt;p&gt;filegrc catalogs them, records how evidence should be collected, and connects their exports or reports to the relevant controls and audit work.&lt;/p&gt;

&lt;p&gt;It also does not decide whether a company is compliant. The organization has to operate its controls, and the independent CPA firm still evaluates the evidence and performs the audit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it locally
&lt;/h2&gt;

&lt;p&gt;You can create a workspace with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-filegrc@latest company-grc
&lt;span class="nb"&gt;cd &lt;/span&gt;company-grc
npm run validate
npm run serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The generated repository starts with draft records and setup work. The starter content is a proposal, not a claim about how your company operates.&lt;/p&gt;

&lt;p&gt;Review it, change it, and commit the version that reflects reality.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Sunpeak-AI" rel="noopener noreferrer"&gt;
        Sunpeak-AI
      &lt;/a&gt; / &lt;a href="https://github.com/Sunpeak-AI/filegrc" rel="noopener noreferrer"&gt;
        filegrc
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Git &amp;amp; file-based SOC 2
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;filegrc&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Run a SOC 2 program as files in Git.&lt;/p&gt;

&lt;p&gt;filegrc gives a founder-led engineering team one place to adopt policies, implement controls, test External Evidence collection, run recurring compliance work, and prepare an audit. JSON holds structured records, Markdown holds long-form work, and Git supplies the change history.&lt;/p&gt;

&lt;p&gt;There is no separate application database. The repository is the program, so engineers and agents can use the same data through the web app, a text editor, or the CLI.&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Sunpeak-AI/filegrc/docs/filegrc-home.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSunpeak-AI%2Ffilegrc%2FHEAD%2Fdocs%2Ffilegrc-home.png" alt="filegrc SOC 2 program overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why it exists&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;SOC 2 work tends to scatter across documents, calendars, tickets, screenshots, and the auditor’s request list. That makes it hard to answer basic questions: What is due? Which policy requires it? What changed during the audit period? Is the evidence complete?&lt;/p&gt;

&lt;p&gt;filegrc keeps that work connected:&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;A starter Security program links criteria references, policies, planned controls, owners, and schedules.&lt;/li&gt;

&lt;li&gt;Work Queue turns policy timing into upcoming, due, and…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Sunpeak-AI/filegrc" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Which security and compliance records would you keep in Git, and which ones belong in their source systems?&lt;/p&gt;

</description>
      <category>startup</category>
      <category>git</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Auditable Agent Context 🔍</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Wed, 15 Jul 2026 17:03:24 +0000</pubDate>
      <link>https://dev.to/abewheeler/auditable-agent-context-jag</link>
      <guid>https://dev.to/abewheeler/auditable-agent-context-jag</guid>
      <description>&lt;p&gt;We just shipped agent context audits at &lt;a href="https://alignbase.ai" rel="noopener noreferrer"&gt;Alignbase&lt;/a&gt;! Now you can know exactly what your agents knew, every time they acted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F931yjj4518tpv48rmq10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F931yjj4518tpv48rmq10.png" alt="Example Alignbase agent context audit record" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When an agent takes action, Alignbase records the context bundle behind that run: the policies, AGENTS.md entries, Skills, versions, tags, and routing decisions that shaped the session. That gives teams a point-in-time record they can inspect later instead of guessing which instructions were present.&lt;/p&gt;

&lt;p&gt;This helps with review, debugging, and governance. If an agent misses a rule or follows stale guidance, you can trace the input path, fix the source context, and prevent the same failure from spreading across the rest of the fleet.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>chatgpt</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Memento Problem</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:18:02 +0000</pubDate>
      <link>https://dev.to/abewheeler/the-memento-problem-bia</link>
      <guid>https://dev.to/abewheeler/the-memento-problem-bia</guid>
      <description>&lt;p&gt;Memento wasn't a great movie, but &lt;a href="https://alignbase.ai/" rel="noopener noreferrer"&gt;Alignbase&lt;/a&gt;'s new Agent Proposals is a great solution to what I like to call "The Memento Problem":&lt;/p&gt;

&lt;p&gt;Agents on the front-line are grappling with the same questions &amp;amp; roadblocks every single session. They come up with their own solutions and complete the task at hand, but those solutions are unshared and discarded. Next startup, they do it all over again.&lt;/p&gt;

&lt;p&gt;Not anymore! Alignbase lets your agents store their learnings in team-wide AGENTS .md and Skill files, and injects them into every agent on your team BEFORE those agents discover the question again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9c3svddgr1ph142dx8rj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9c3svddgr1ph142dx8rj.png" alt="Alignbase suggestion from Codex to improve a company overview AGENTS.md" width="561" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alignbase is the place for teams &amp;amp; agent fleets to manage shared context and learn as a collective! It's a way better solution than tattoos.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>chatgpt</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Alignbase Skills Repository</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Thu, 02 Jul 2026 16:23:13 +0000</pubDate>
      <link>https://dev.to/abewheeler/the-alignbase-skills-repository-lcb</link>
      <guid>https://dev.to/abewheeler/the-alignbase-skills-repository-lcb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfhapeh37ptdhabonfuf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfhapeh37ptdhabonfuf.png" alt="Alignbase sidebar with team's shared Context and Skills" width="754" height="1124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Introducing the &lt;a href="https://alignbase.ai/" rel="noopener noreferrer"&gt;Alignbase&lt;/a&gt; Skills repository! With Skills, Alignbase is the place for teams to collaborate on ALL shared context, from the context your team's agents MUST use (like AGENTS.md) to the context they CAN use (Skills).&lt;/p&gt;

&lt;p&gt;Today, Alignbase brings fully managed context (auth, permissions, audits, reviews, and versioning) and a single source of truth to your internal Skills as well.&lt;/p&gt;

&lt;p&gt;Like all things Alignbase, the Skills repository works in the background across Claude Code, Cowork, Codex, Cursor, ChatGPT, or anywhere you use models, whether that's web, desktop, CLI, or some other custom harness!&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>chatgpt</category>
    </item>
    <item>
      <title>We need a company-wide AGENTS.md</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Tue, 23 Jun 2026 11:48:51 +0000</pubDate>
      <link>https://dev.to/abewheeler/we-need-a-company-wide-agentsmd-5oi</link>
      <guid>https://dev.to/abewheeler/we-need-a-company-wide-agentsmd-5oi</guid>
      <description>&lt;p&gt;Introducing &lt;a href="https://alignbase.ai/" rel="noopener noreferrer"&gt;Alignbase&lt;/a&gt;: your company-wide AGENTS.md&lt;/p&gt;

&lt;p&gt;The engineers that used to write the code knew the company, product, and policies. Now, the agents writing the code don’t.&lt;/p&gt;

&lt;p&gt;It’s easy to give agents access to the internal wiki or Skills repo, and those solutions have their place. It’s one thing to give agents context they CAN use and another to inject context they MUST use.&lt;/p&gt;

&lt;p&gt;The AGENTS.md forces context uptake within a code repository, but these files are static and they don’t cross repo, user, and web agent boundaries cleanly. Alignbase unifies shared context under a single remote repository.&lt;/p&gt;

&lt;p&gt;Context needs to be treated as a first-class citizen. It’s scarce and it’s valuable. Context needs optimization, authentication, permissions, audits, and approvals. Alignbase does that. Wikis, Skills, AGENTS.md, and “company brains” don’t.&lt;/p&gt;

&lt;p&gt;That shared Github repo isn’t enough. Who’s allowed to define what the company is? What the team’s policies are? Is that enforced? Is access authenticated? Is delivery dynamic? How do you immediately tell all your company’s agents about your ongoing AWS outage?&lt;/p&gt;

&lt;p&gt;Alignbase is the first shared context repository and context control plane. Grateful for any and all feedback!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://alignbase.ai/" rel="noopener noreferrer"&gt;https://alignbase.ai/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>claude</category>
    </item>
    <item>
      <title>How do you know budget models are smart enough for your MCP server?</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Tue, 14 Apr 2026 18:23:12 +0000</pubDate>
      <link>https://dev.to/abewheeler/how-do-you-know-budget-models-are-smart-enough-for-your-mcp-server-1di3</link>
      <guid>https://dev.to/abewheeler/how-do-you-know-budget-models-are-smart-enough-for-your-mcp-server-1di3</guid>
      <description>&lt;p&gt;We just shipped evals for sunpeak.ai&lt;/p&gt;

&lt;p&gt;The #1 thing I hear from MCP server teams: “Our tools worked great with the latest models, but we had to start from scratch when we realized the free models couldn't use them at all.”&lt;/p&gt;

&lt;p&gt;Budget models call tools differently: they misread ambiguous schemas, they pass wrong arguments, they can't chain tool calls, and you don’t find out until users complain.&lt;/p&gt;

&lt;p&gt;sunpeak evals test your MCP server across every model that matters, in one command. 100% on GPT-4o, 40% on Gemini Flash. That 40% is a schema problem you’d never catch testing manually on ChatGPT. Fix the tool architecture + description, run it again, and watch it climb to 95%.&lt;/p&gt;

&lt;p&gt;Works with any MCP server. sunpeak connects over MCP, discovers your tools, and runs each eval case dozens of times per model so you get a real pass rate, not a single lucky result.&lt;/p&gt;

&lt;p&gt;Put it in CI. Track reliability over time. Your MCP server isn’t production-ready until the cheapest model your users might connect it to can use it consistently.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>🚀 MCP App Testing Framework!</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Wed, 08 Apr 2026 17:15:13 +0000</pubDate>
      <link>https://dev.to/abewheeler/mcp-app-testing-framework-4bao</link>
      <guid>https://dev.to/abewheeler/mcp-app-testing-framework-4bao</guid>
      <description>&lt;p&gt;We just shipped sunpeak.ai as a standalone testing framework for MCP Apps!&lt;/p&gt;

&lt;p&gt;If you're building MCP Apps for ChatGPT or Claude, you know the pain: deploy, open the host, start a conversation, trigger the tool, check the result. Repeat for both hosts, both themes, three display modes. That's 24 combinations per code change.&lt;/p&gt;

&lt;p&gt;sunpeak replicates the ChatGPT and Claude runtimes locally. You write Playwright tests that call tools, render resources, and assert against the output. One test file runs against both hosts automatically.&lt;/p&gt;

&lt;p&gt;What's included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit tests (Vitest + happy-dom)&lt;/li&gt;
&lt;li&gt;E2E tests against replicated ChatGPT and Claude runtimes&lt;/li&gt;
&lt;li&gt;Visual regression testing with screenshot baselines&lt;/li&gt;
&lt;li&gt;Live tests against real ChatGPT&lt;/li&gt;
&lt;li&gt;Works with any MCP server in any language (Python, Go, TypeScript)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add it to an existing project with one command:&lt;/p&gt;

&lt;p&gt;pnpm add -g sunpeak &amp;amp;&amp;amp; sunpeak test init&lt;/p&gt;

&lt;p&gt;No paid host accounts. No AI credits. Runs in CI/CD.&lt;/p&gt;

&lt;p&gt;MIT licensed and open source! &lt;a href="https://sunpeak.ai/testing-framework/" rel="noopener noreferrer"&gt;https://sunpeak.ai/testing-framework/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>chatgpt</category>
      <category>webdev</category>
    </item>
    <item>
      <title>MCP Apps are hard to test</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Wed, 25 Mar 2026 16:39:12 +0000</pubDate>
      <link>https://dev.to/abewheeler/mcp-apps-are-hard-to-test-52d5</link>
      <guid>https://dev.to/abewheeler/mcp-apps-are-hard-to-test-52d5</guid>
      <description>&lt;p&gt;MCP Apps are hard to test.&lt;/p&gt;

&lt;p&gt;They run inside ChatGPT and Claude, so every code change means deploying to a real host, burning AI credits, and waiting through non-deterministic LLM responses. If you're building for both hosts, double everything.&lt;/p&gt;

&lt;p&gt;We built the &lt;a href="https://sunpeak.ai/mcp-app-inspector/" rel="noopener noreferrer"&gt;sunpeak Inspector&lt;/a&gt; to fix this.&lt;/p&gt;

&lt;p&gt;It replicates the ChatGPT and Claude MCP App runtimes on localhost. Your app renders exactly as it would inside the real hosts, with accurate display modes, themes, safe areas, and conversation chrome. One command to start:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sunpeak inspect --server URL&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Works with any MCP server. Python, TypeScript, Go, whatever. No sunpeak project required.&lt;/p&gt;

&lt;p&gt;For development: Switch between ChatGPT and Claude from the sidebar. Toggle light/dark themes, mobile/tablet/desktop widths, and display modes. Edit tool input and output live. Changes appear instantly with HMR.&lt;/p&gt;

&lt;p&gt;For testing: The inspector doubles as the test runtime for Playwright E2E tests. Define tool states with simulation files (JSON fixtures), load them via URL, and assert against the rendered output. Test every host, theme, and display mode combination in CI/CD. No paid accounts, no API keys, no credits on your CI runners.&lt;/p&gt;

&lt;p&gt;For coding agents: Claude Code, Codex, and Cursor can run the inspector and execute Playwright tests programmatically, so they can iterate on MCP Apps without needing a human to manually test in a real host.&lt;br&gt;
sunpeak is MIT licensed and open source.&lt;a href="https://dev.tourl"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Live Testing for Claude Connectors and ChatGPT Apps</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Wed, 18 Mar 2026 18:43:24 +0000</pubDate>
      <link>https://dev.to/abewheeler/live-testing-for-claude-connectors-and-chatgpt-apps-35lh</link>
      <guid>https://dev.to/abewheeler/live-testing-for-claude-connectors-and-chatgpt-apps-35lh</guid>
      <description>&lt;p&gt;The sunpeak simulator tests cover a lot. They replicate the ChatGPT and Claude runtimes, run display mode transitions, test themes, and validate tool invocations without any paid accounts or AI credits. For most development work, they're enough.&lt;/p&gt;

&lt;p&gt;But simulators don't catch everything. Real ChatGPT wraps your app in a nested iframe sandbox. The MCP protocol goes through ChatGPT's actual connection layer. Resource loading happens over a real network with production builds. There's a gap between "works in the simulator" and "works in ChatGPT," and the only way to close it is to test against the real thing.&lt;/p&gt;

&lt;p&gt;sunpeak 0.16.23 adds live testing: automated Playwright tests that run against real ChatGPT. You write the same kind of assertions you write for simulator tests, and sunpeak handles authentication, MCP server refresh, host-specific message formatting, and iframe traversal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Run &lt;code&gt;pnpm test:live&lt;/code&gt; with a tunnel active. sunpeak imports your browser session, starts the dev server, refreshes the MCP connection, and runs your &lt;code&gt;tests/live/*.spec.ts&lt;/code&gt; files in parallel against real ChatGPT. You write assertions against the app iframe. Everything else is automated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Live Tests Actually Do
&lt;/h2&gt;

&lt;p&gt;A live test opens a real ChatGPT session in a browser, types a message that triggers your &lt;a href="https://sunpeak.ai/docs/mcp-apps/mcp/tools" rel="noopener noreferrer"&gt;MCP tool&lt;/a&gt;, waits for ChatGPT to call it, and then asserts against the rendered app inside the host's iframe.&lt;/p&gt;

&lt;p&gt;Here's a complete live test for an albums resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;albums tool renders photo grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-albums&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summer Slice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Switch to dark mode without re-invoking the tool&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setColorScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summer Slice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;live.invoke('show-albums')&lt;/code&gt; starts a new chat, sends &lt;code&gt;/{appName} show-albums&lt;/code&gt; to ChatGPT, waits for the LLM response to finish streaming, waits for the app iframe to render, and returns a Playwright &lt;code&gt;FrameLocator&lt;/code&gt; pointed at your app's content. From there, it's standard Playwright assertions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;{ timeout: 15_000 }&lt;/code&gt; accounts for the LLM response time. ChatGPT needs to process your message, decide to call the tool, receive the result, and render the iframe. In practice this takes 5 to 10 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;ChatGPT account&lt;/strong&gt; with MCP/Apps support (Plus or higher)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tunnel&lt;/strong&gt; tool like &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; or &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Your MCP server &lt;strong&gt;connected in ChatGPT&lt;/strong&gt; (Settings &amp;gt; Apps &amp;gt; Create, enter your tunnel URL with &lt;code&gt;/mcp&lt;/code&gt; path)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You do not need to install anything extra in your sunpeak project. Live test infrastructure ships with &lt;code&gt;sunpeak&lt;/code&gt; starting at v0.16.23. New projects scaffolded with &lt;code&gt;sunpeak new&lt;/code&gt; include example live test specs and the Playwright config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Live Tests
&lt;/h2&gt;

&lt;p&gt;Open two terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1: Start a tunnel&lt;/span&gt;
ngrok http 8000

&lt;span class="c"&gt;# Terminal 2: Run live tests&lt;/span&gt;
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first run, sunpeak imports your ChatGPT session from your browser. It checks Chrome, Arc, Brave, and Edge automatically. If no valid session is found, it opens a browser window and waits for you to log in. The session is saved to &lt;code&gt;tests/live/.auth/chatgpt.json&lt;/code&gt; and reused for 24 hours.&lt;/p&gt;

&lt;p&gt;After authentication, sunpeak:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Starts &lt;code&gt;sunpeak dev --prod-resources&lt;/code&gt; (production resource builds)&lt;/li&gt;
&lt;li&gt;Navigates to ChatGPT Settings &amp;gt; Apps, finds your MCP server, and clicks Refresh&lt;/li&gt;
&lt;li&gt;Runs all &lt;code&gt;tests/live/*.spec.ts&lt;/code&gt; files fully in parallel, each in its own chat window&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The MCP refresh happens once in &lt;code&gt;globalSetup&lt;/code&gt;, before any test workers start. This means your test workers don't each individually refresh the connection, which would be slow and flaky.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fixture API
&lt;/h2&gt;

&lt;p&gt;All live tests import from &lt;code&gt;sunpeak/test&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;test&lt;/code&gt; function provides a &lt;code&gt;live&lt;/code&gt; fixture with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invoke(prompt)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starts a new chat, sends the prompt with host-specific formatting, waits for the app iframe, returns a &lt;code&gt;FrameLocator&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendMessage(text)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sends a message in the current chat with &lt;code&gt;/{appName}&lt;/code&gt; prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendRawMessage(text)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sends a message without any prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startNewChat()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens a fresh conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waitForAppIframe()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Waits for the MCP app iframe and returns a &lt;code&gt;FrameLocator&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setColorScheme(scheme, appFrame?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Switches to &lt;code&gt;'light'&lt;/code&gt; or &lt;code&gt;'dark'&lt;/code&gt; via &lt;code&gt;page.emulateMedia()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raw Playwright &lt;code&gt;Page&lt;/code&gt; object&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most tests only need &lt;code&gt;invoke&lt;/code&gt; and &lt;code&gt;setColorScheme&lt;/code&gt;. The &lt;code&gt;invoke&lt;/code&gt; method handles the full flow: new chat, message formatting (ChatGPT requires &lt;code&gt;/{appName}&lt;/code&gt; before your prompt), waiting for streaming to finish, waiting for the nested iframe to render, and returning a locator into your app's content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Theme Testing Without Re-Invocation
&lt;/h2&gt;

&lt;p&gt;Sending a second message to trigger a new tool call is slow and burns credits. &lt;code&gt;setColorScheme&lt;/code&gt; avoids that by switching the browser's &lt;code&gt;prefers-color-scheme&lt;/code&gt; via Playwright's &lt;code&gt;page.emulateMedia()&lt;/code&gt;. ChatGPT propagates the change into the iframe, and your app re-renders with the new theme.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ticket card text stays readable in dark mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-ticket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Search results not loading on mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify status badge and assignee are visible in light mode&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sarah Chen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Switch to dark mode — common bugs: text blends into background,&lt;/span&gt;
  &lt;span class="c1"&gt;// borders disappear, badge colors lose contrast&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setColorScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Same elements should still be visible with the new theme applied&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sarah Chen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Badge background should still be distinguishable from the card&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;badge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span:has-text("high")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;badgeBg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;badge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;backgroundColor&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;badgeBg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(0, 0, 0, 0)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second argument to &lt;code&gt;setColorScheme&lt;/code&gt; tells it to wait for the app's &lt;code&gt;&amp;lt;html data-theme="dark"&amp;gt;&lt;/code&gt; attribute to confirm the theme propagated through the iframe boundary before your assertions run.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Full Example
&lt;/h2&gt;

&lt;p&gt;Here's a live test for a review card resource. It invokes the tool, checks the rendered content, verifies a button interaction triggers a state transition, and confirms the card re-themes correctly in dark mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;review card renders and handles approval flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;review-diff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify the card rendered with the right content&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Refactor Authentication Module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Action buttons present&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applyButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Apply Changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applyButton&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Theme switch: card should stay readable in dark mode&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setColorScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applyButton&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Click Apply Changes — UI transitions to accepted state&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;applyButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applyButton&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text=Applying changes...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches real issues that simulator tests can miss: the iframe sandbox blocking a script load, a theme change not propagating through the nested iframe boundary, or a button click failing because of host-specific event handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Playwright Config
&lt;/h2&gt;

&lt;p&gt;The live test config is a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/live/playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineLiveConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineLiveConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a full Playwright config with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;globalSetup&lt;/code&gt;&lt;/strong&gt; pointing to sunpeak's auth and MCP refresh flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;headless: false&lt;/code&gt;&lt;/strong&gt; because chatgpt.com blocks headless browsers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-bot browser arguments&lt;/strong&gt; and a real Chrome user agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2-minute timeout per test&lt;/strong&gt; (LLM responses can be slow)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 retry&lt;/strong&gt; per test (LLM responses are non-deterministic)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully parallel execution&lt;/strong&gt; (each test gets its own chat)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic dev server&lt;/strong&gt; with &lt;code&gt;--prod-resources&lt;/code&gt; on a dynamically allocated port&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can pass options to customize the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineLiveConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;colorScheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr-FR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Europe/Paris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.8566&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.3522&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Relates to Simulator Tests
&lt;/h2&gt;

&lt;p&gt;Live tests don't replace &lt;a href="https://dev.to/blogs/complete-guide-testing-chatgpt-apps"&gt;simulator tests&lt;/a&gt;. They complement them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Simulator (&lt;code&gt;pnpm test:e2e&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Live (&lt;code&gt;pnpm test:live&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs against&lt;/td&gt;
&lt;td&gt;Local simulator&lt;/td&gt;
&lt;td&gt;Real ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;td&gt;10-30 seconds per test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Requires ChatGPT Plus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not recommended (needs auth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catches&lt;/td&gt;
&lt;td&gt;Component logic, display modes, themes, cross-host layout&lt;/td&gt;
&lt;td&gt;Real MCP connection, LLM tool invocation, iframe sandbox, production resource loading&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use simulator tests for development and CI/CD. Use live tests before shipping, after major changes, or when debugging issues that only reproduce in the real host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Pyramid for Claude Connectors
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://dev.to/blogs/what-are-claude-connectors"&gt;Claude Connector&lt;/a&gt; built with sunpeak now has three test tiers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; (&lt;code&gt;pnpm test&lt;/code&gt;): Vitest, jsdom, fast, test component logic in isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulator e2e tests&lt;/strong&gt; (&lt;code&gt;pnpm test:e2e&lt;/code&gt;): Playwright against the local &lt;a href="https://dev.to/blogs/claude-simulator-for-mcp-apps"&gt;ChatGPT and Claude simulator&lt;/a&gt;, test display modes and themes, runs in &lt;a href="https://dev.to/blogs/mcp-app-github-actions-cicd"&gt;CI/CD&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live tests&lt;/strong&gt; (&lt;code&gt;pnpm test:live&lt;/code&gt;): Playwright against real ChatGPT (with Claude coming soon), test real MCP protocol behavior and iframe rendering&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each tier catches different classes of bugs. Unit tests catch logic errors. Simulator tests catch rendering and layout issues across hosts and display modes. Live tests catch protocol and sandbox issues that only show up in the real host environment.&lt;/p&gt;

&lt;p&gt;All three are pre-configured when you run &lt;code&gt;sunpeak new&lt;/code&gt;. You don't need to set up Vitest, Playwright, or any test infrastructure yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Host-Agnostic Architecture
&lt;/h2&gt;

&lt;p&gt;The live test infrastructure is designed to support multiple hosts. The &lt;code&gt;live&lt;/code&gt; fixture resolves the correct host page object based on the Playwright project name. All host-specific DOM interaction (selectors, login flow, settings navigation, iframe nesting) lives in per-host page objects that sunpeak maintains.&lt;/p&gt;

&lt;p&gt;Your test code is host-agnostic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my resource renders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show me something&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This same test will run against any host that sunpeak supports. Today that's ChatGPT. When Claude live testing ships, add it with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/live/playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineLiveConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chatgpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No changes to your test files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you have an existing sunpeak project, update to v0.16.23 or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add sunpeak@latest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sunpeak upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;tests/live/playwright.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineLiveConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineLiveConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the test script to &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:live"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright test --config tests/live/playwright.config.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write your first live test in &lt;code&gt;tests/live/your-resource.spec.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my tool renders correctly in ChatGPT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your prompt here&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-selector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a tunnel, run &lt;code&gt;pnpm test:live&lt;/code&gt;, and watch Playwright drive a real ChatGPT session.&lt;/p&gt;

&lt;p&gt;New projects created with &lt;code&gt;sunpeak new&lt;/code&gt; include all of this out of the box, with example live tests for every starter resource.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Claude Simulator for MCP Apps: Test Claude Apps Locally with sunpeak</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Mon, 02 Mar 2026 15:15:24 +0000</pubDate>
      <link>https://dev.to/abewheeler/claude-simulator-for-mcp-apps-test-claude-apps-locally-with-sunpeak-2a1g</link>
      <guid>https://dev.to/abewheeler/claude-simulator-for-mcp-apps-test-claude-apps-locally-with-sunpeak-2a1g</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; sunpeak v0.15 adds a local Claude simulator. Run &lt;code&gt;sunpeak dev&lt;/code&gt;, pick Claude from the Host dropdown (or &lt;code&gt;?host=claude&lt;/code&gt; URL Param), and test your MCP App in both Claude and ChatGPT from one dev server.&lt;/p&gt;

&lt;p&gt;Until now, sunpeak's local simulator only replicated the ChatGPT runtime. If you wanted to test how your &lt;a href="https://dev.to/mcp-app-framework"&gt;MCP App&lt;/a&gt; looked in Claude, you had to deploy it and connect it manually. That's fixed.&lt;/p&gt;

&lt;p&gt;sunpeak v0.15 ships with first-class Claude support. The old &lt;code&gt;ChatGPTSimulator&lt;/code&gt; is now just &lt;code&gt;Simulator&lt;/code&gt;, and both Claude and ChatGPT are registered as host shells out of the box. Switch between them with a dropdown, a URL param, or a prop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;The simulator is now multi-host. Instead of a single ChatGPT-specific component, sunpeak uses a pluggable host shell system. Each host registers its own conversation chrome, color palette, and theme behavior. The &lt;code&gt;Simulator&lt;/code&gt; component renders whichever host you select.&lt;/p&gt;

&lt;p&gt;Two hosts ship by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT&lt;/strong&gt; uses the familiar gray/white palette with the ChatGPT conversation layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; uses a warm beige/cream palette matching claude.ai, with Claude's conversation chrome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both implement the core MCP App protocol, but each host adds its own extras. ChatGPT supports host-specific features like file uploads and downloads on top of the standard. Claude doesn't have additional host APIs today, though sunpeak's Claude host shell does handle Claude's rendering quirks. If Claude adds host-specific capabilities in the future, they'll be built into this shell. Your resource component renders in both, wrapped in each host's chat UI, so you see exactly what your users will see.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use It
&lt;/h2&gt;

&lt;p&gt;If you already have a sunpeak project, update to v0.15 and &lt;a href="https://github.com/Sunpeak-AI/sunpeak/releases/tag/v0.15.1" rel="noopener noreferrer"&gt;migrate your CSS&lt;/a&gt; classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sunpeak upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sunpeak dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;localhost:3000&lt;/code&gt;. You will see a Host dropdown in the simulator sidebar. Select &lt;strong&gt;Claude&lt;/strong&gt; to test your app in the Claude runtime. Select &lt;strong&gt;ChatGPT&lt;/strong&gt; to switch back.&lt;/p&gt;

&lt;p&gt;If you are starting fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-g&lt;/span&gt; sunpeak
sunpeak new
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sunpeak dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scaffolded project uses the new &lt;code&gt;Simulator&lt;/code&gt; component by default. Both hosts are available from the first run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Host Selection
&lt;/h2&gt;

&lt;p&gt;Three ways to pick a host:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidebar dropdown.&lt;/strong&gt; The Host control appears in the sidebar when multiple hosts are registered. Click it to switch at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL parameter.&lt;/strong&gt; Add &lt;code&gt;?host=claude&lt;/code&gt; or &lt;code&gt;?host=chatgpt&lt;/code&gt; to the simulator URL. This is useful for bookmarking a specific host, linking teammates to a particular test configuration, or testing certain rendering states automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;defaultHost&lt;/code&gt; prop.&lt;/strong&gt; Set the initial host in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Simulator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/simulator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Simulator&lt;/span&gt;
  &lt;span class="na"&gt;simulations&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;simulations&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;defaultHost&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"claude"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is &lt;code&gt;chatgpt&lt;/code&gt; if you don't specify one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from ChatGPTSimulator
&lt;/h2&gt;

&lt;p&gt;If your project uses the old &lt;code&gt;ChatGPTSimulator&lt;/code&gt; from &lt;code&gt;sunpeak/chatgpt&lt;/code&gt;, it still work as an alias to the new simulator. No migration is required, but the alias will be removed in the near future.&lt;/p&gt;

&lt;p&gt;The change is small. In your dev entry point, replace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatGPTSimulator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/chatgpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatGPTSimulator&lt;/span&gt; &lt;span class="na"&gt;simulations&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;simulations&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Simulator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/simulator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Simulator&lt;/span&gt; &lt;span class="na"&gt;simulations&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;simulations&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same simulations, same resource components, same test suite. The &lt;code&gt;Simulator&lt;/code&gt; just adds the host dropdown and Claude's rendering behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Your App Looks Like in Claude
&lt;/h2&gt;

&lt;p&gt;The Claude host shell wraps your resource component in Claude's conversation UI. The background uses Claude's warm beige and grey instead of ChatGPT's white and dark grey. User messages appear in Claude's bubble style. The toolbar and display mode controls (inline, fullscreen, picture-in-picture) work the same way.&lt;/p&gt;

&lt;p&gt;The core data flow is shared across hosts. &lt;a href="https://sunpeak.ai/docs/mcp-apps/react/use-app" rel="noopener noreferrer"&gt;&lt;code&gt;useToolData&lt;/code&gt;&lt;/a&gt; receives the tool output. &lt;code&gt;useAppState&lt;/code&gt; syncs state back to the host. &lt;code&gt;SafeArea&lt;/code&gt; handles safe rendering boundaries. These work the same in both Claude and ChatGPT.&lt;/p&gt;

&lt;p&gt;Where hosts differ is in extras. ChatGPT supports host-specific features like file uploads and downloads that go beyond the MCP App standard. Claude has its own rendering quirks that sunpeak's host shell accounts for. If Claude adds host-specific APIs later, sunpeak will surface them through the same shell system.&lt;/p&gt;

&lt;p&gt;The simulator lets you catch these differences locally instead of deploying to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensible Host System
&lt;/h2&gt;

&lt;p&gt;The host shell registry is open. If a new major MCP App host appears, sunpeak can add support without changing the &lt;code&gt;Simulator&lt;/code&gt; component or your resource code. Each host registers itself with an &lt;code&gt;id&lt;/code&gt;, a &lt;code&gt;label&lt;/code&gt;, a conversation component, a &lt;a href="https://sunpeak.ai/docs/mcp-apps/styling/theme" rel="noopener noreferrer"&gt;theme function&lt;/a&gt;, and &lt;a href="https://sunpeak.ai/docs/mcp-apps/styling/css-variables" rel="noopener noreferrer"&gt;style variables&lt;/a&gt;. The simulator picks up all registered hosts automatically.&lt;/p&gt;

&lt;p&gt;For now, Claude and ChatGPT cover the two largest MCP App hosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-g&lt;/span&gt; sunpeak
sunpeak new
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sunpeak dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;localhost:3000&lt;/code&gt;, select Claude from the Host dropdown, and start building.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/how-to-build-a-claude-app"&gt;How to Build a Claude App&lt;/a&gt; covers architecture and code patterns for Claude.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/chatgpt-app-tutorial"&gt;ChatGPT App Tutorial&lt;/a&gt; walks through building a resource from scratch (same steps work for Claude).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/build-mcp-app-for-chatgpt-and-claude"&gt;Building One MCP App for ChatGPT and Claude&lt;/a&gt; covers the cross-platform story.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/complete-guide-testing-chatgpt-apps"&gt;Testing guide&lt;/a&gt; covers Vitest and Playwright setup.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sunpeak.ai/docs/quickstart" rel="noopener noreferrer"&gt;sunpeak documentation&lt;/a&gt; has the quickstart and full API reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build once with &lt;a href="https://dev.to/"&gt;sunpeak&lt;/a&gt;, test locally in both Claude and ChatGPT, and ship to every &lt;a href="https://dev.to/mcp-app-framework"&gt;MCP App host&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>sunpeak is all-in on MCP Apps</title>
      <dc:creator>Abe Wheeler</dc:creator>
      <pubDate>Wed, 11 Feb 2026 17:18:22 +0000</pubDate>
      <link>https://dev.to/abewheeler/sunpeak-is-all-in-on-mcp-apps-2lg8</link>
      <guid>https://dev.to/abewheeler/sunpeak-is-all-in-on-mcp-apps-2lg8</guid>
      <description>&lt;p&gt;MCP Apps now run in ChatGPT, Claude, Goose, and VS Code. That happened fast. Claude &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/" rel="noopener noreferrer"&gt;announced MCP App support&lt;/a&gt; on January 26, and ChatGPT &lt;a href="https://developers.openai.com/apps-sdk/mcp-apps-in-chatgpt" rel="noopener noreferrer"&gt;followed on February 4&lt;/a&gt;. Two weeks, two major hosts, one standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; sunpeak's APIs are built around the MCP App standard. ChatGPT and Claude-specific features are layered on top as optional imports. Write your app once, run it everywhere—even localhost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP-App-First
&lt;/h2&gt;

&lt;p&gt;When ChatGPT Apps launched in October 2025, they had their own proprietary SDK. Building for ChatGPT meant building &lt;em&gt;only&lt;/em&gt; for ChatGPT.&lt;/p&gt;

&lt;p&gt;That changed when OpenAI contributed to and aligned on &lt;a href="https://github.com/modelcontextprotocol/ext-apps" rel="noopener noreferrer"&gt;MCP Apps&lt;/a&gt; as the open standard. The rendering model, the iframe sandbox, the UI functionality — all of it became portable. And as of February 2026, the major hosts actually implemented it.&lt;/p&gt;

&lt;p&gt;sunpeak followed the same trajectory. We started as a ChatGPT App framework because ChatGPT was the only major host supporting embedded UIs. Now we're an MCP App framework, because the standard is real and the host list is growing.&lt;/p&gt;

&lt;p&gt;What that means in practice: sunpeak's core APIs target the MCP App interface, not any single host. At the same time, sunpeak layers in the major host-specific functionality developers need to seamlessly support differentiated platforms (think React Native).&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;sunpeak separates standard MCP App APIs from host-specific ones at the import level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core APIs&lt;/strong&gt; come from the top-level &lt;code&gt;sunpeak&lt;/code&gt; import. These work everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useToolData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useHostContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useDisplayMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AppProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ResourceConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResourceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Show analytics dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DashboardResource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useToolData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useHostContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;displayMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDisplayMode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Your UI — runs in ChatGPT, Claude, Goose, VS Code */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Host-specific APIs&lt;/strong&gt; come from subpath imports. Right now that's &lt;code&gt;sunpeak/chatgpt&lt;/code&gt; for ChatGPT-specific tooling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChatGPTSimulator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buildDevSimulations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sunpeak/chatgpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ChatGPT simulator, dev simulation builder, and any ChatGPT-only runtime features live here. They're first-class — not afterthoughts or community plugins — but they don't pollute your app code. Your resource components stay portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed, What Didn't
&lt;/h2&gt;

&lt;p&gt;If you're already using sunpeak, you'll notice &lt;code&gt;v0.13&lt;/code&gt; changes many APIs to be based on MCP App abstractions and nomenclature.&lt;br&gt;
Fortunately, a lot of Apps SDK knowledge is easily portable to the MCP App interface.&lt;br&gt;
Refer to the &lt;a href="https://github.com/Sunpeak-AI/sunpeak/releases/tag/v0.13.1" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; for more specific migration instructions,&lt;br&gt;
and refer to the &lt;a href="https://docs.sunpeak.ai/" rel="noopener noreferrer"&gt;sunpeak docs&lt;/a&gt; for a complete overview of sunpeak and MCP Apps.&lt;/p&gt;

&lt;p&gt;With these changes, your app renders in Claude (and others) today. The &lt;code&gt;sunpeak dev&lt;/code&gt; simulator at &lt;code&gt;localhost:6767&lt;/code&gt; replicates the MCP App runtime that all hosts implement, so what works locally works in production across hosts.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Host Landscape
&lt;/h2&gt;

&lt;p&gt;Here's where MCP Apps run today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT&lt;/strong&gt; — OpenAI contributed elements of the original ChatGPT Apps protocol to MCP and now supports the open standard alongside their existing SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude&lt;/strong&gt; — Anthropic's web and desktop clients render MCP Apps natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goose&lt;/strong&gt; — Block's open-source AI agent supports MCP Apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code Insiders&lt;/strong&gt; — Microsoft's editor renders MCP Apps in the chat sidebar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More hosts will follow. The MCP App standard is under the Linux Foundation now, and the spec is actively developed at &lt;a href="https://github.com/modelcontextprotocol/ext-apps" rel="noopener noreferrer"&gt;modelcontextprotocol/ext-apps&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Platform-Specific Features Are First-Class
&lt;/h2&gt;

&lt;p&gt;MCP-App-first doesn't mean lowest-common-denominator. ChatGPT has features that Claude doesn't, and vice versa. sunpeak treats these as first-class extensions, not hacks.&lt;/p&gt;

&lt;p&gt;For ChatGPT, that means full access to OpenAI's &lt;a href="https://github.com/openai/apps-sdk-ui" rel="noopener noreferrer"&gt;apps-sdk-ui&lt;/a&gt; component library, the ChatGPT simulator for local development, and any ChatGPT-specific runtime APIs. These are maintained alongside the core framework, tested in CI, and documented.&lt;/p&gt;

&lt;p&gt;As Claude and other hosts ship their own platform-specific features, sunpeak will add corresponding subpath imports. The pattern scales: core stays portable, extensions stay organized.&lt;/p&gt;
&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;sunpeak is open source and free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-g&lt;/span&gt; sunpeak &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sunpeak new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app works across ChatGPT, Claude, and every other MCP App host from the first line of code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.sunpeak.ai" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;: guides, API reference, and tutorials&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Sunpeak-AI/sunpeak" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;: source code and issue tracker&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sunpeak.ai/chatgpt-app-framework" rel="noopener noreferrer"&gt;MCP App Framework&lt;/a&gt;: overview of sunpeak's capabilities&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>webdev</category>
      <category>react</category>
    </item>
  </channel>
</rss>
