photon

Build Photon MCPs — single-file TypeScript MCP servers. Use for creating photons with @format annotations (table, chart:bar), stateful photons using this.memory for persistent storage, photons with @readOnly/@destructive annotations, custom UI using @ui tags/HTML templates, photons wrapping APIs (Stripe, payments), task scheduler photons with cron, user-configurable settings (protected settings), this.render() for live output, photon build for standalone binaries, mermaid diagrams for photon architecture, editing .photon.ts files. DO NOT trigger for general TypeScript or non-photon MCP.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "photon" with this command: npx skills add portel-dev/skills/portel-dev-skills-photon

Photon Development Guide

Photons are single-file TypeScript MCP servers. No compilation — runs directly with tsx.

Quick Start

npm install -g @portel/photon    # Install runtime
photon maker new my-weather      # Create photon
photon beam                      # Launch Beam UI
photon mcp my-weather            # Run as MCP server
photon cli my-weather current --city London  # CLI

Files go in ~/.photon/. See Directory Structure for the full layout. Connect to Claude Desktop:

{ "mcpServers": { "my-weather": { "command": "photon", "args": ["mcp", "my-weather"] } } }

Minimal Photon

import { Photon } from '@portel/photon-core';

/**
 * Weather API
 * @version 1.0.0
 * @dependencies axios@^1.0.0
 * @icon 🌤️
 */
export default class Weather extends Photon {
  constructor(private apiKey: string) { super(); }

  /**
   * Get current weather for a city
   * @param city City name {@example London}
   * @readOnly
   * @title Current Weather
   * @format markdown
   */
  async current({ city }: { city: string }): Promise<string> {
    const res = await fetch(`https://api.weather.com/v1/current?q=${city}&key=${this.apiKey}`);
    if (!res.ok) throw new Error(`API error: ${res.statusText}`);
    const data = await res.json();
    return `**${data.name}** — ${data.temp}°C, ${data.description}`;
  }
}

Core Principles

  1. Return values directly — no { success: true, data } wrappers. If it returns, it succeeded.
  2. Throw on errors — let the runtime handle them. Don't catch and wrap.
  3. Single-word method names — they read as CLI commands: weather current, not weather getCurrentWeather.
  4. Constructor = config — constructor params auto-map to env vars: PHOTON_WEATHER_APIKEY.

Class Structure

/**
 * Brief description (becomes photon description)
 *
 * @version 1.0.0
 * @runtime ^1.5.0
 * @dependencies package1, package2@^2.0.0
 * @icon 🔧
 * @tags api, utility
 */
export default class MyTool extends Photon {
  constructor(private apiKey: string) { super(); }

  /**
   * Method description (becomes tool description)
   * @param query Search query {@example "typescript"} {@min 1}
   * @readOnly
   * @title Search Items
   * @format table
   */
  async search({ query }: { query: string }) { ... }
}

MCP Annotations

JSDoc tags map to MCP protocol annotations (spec 2025-11-25):

/**
 * @readOnly        — no side effects, safe to auto-approve
 * @destructive     — requires confirmation
 * @idempotent      — safe to retry
 * @openWorld       — calls external systems
 * @closedWorld     — local data only
 * @title My Tool   — human-readable display name
 * @audience user   — who sees results: user, assistant, or both
 * @priority 0.9    — content importance (0.0–1.0)
 */

UI-only methods: Combine @internal + @audience user for methods the dashboard can call but the LLM never sees:

/**
 * Dashboard-only admin panel data.
 * @internal
 * @audience user
 * @readOnly
 */
async metrics() { return { cpu: 42 }; }

@internal hides from tools/list. @audience user marks results as human-only. The UI still calls via window.photon.callTool('metrics', {}).

Structured Output

Auto-generated from TypeScript return types — no tags needed:

async create(params: { title: string }): Promise<{ id: string; done: boolean }> { ... }

For field descriptions, use an interface with JSDoc:

interface Task {
  /** Unique identifier */
  id: string;
  /** Whether complete */
  done: boolean;
}
async create(params: { title: string }): Promise<Task> { ... }

Output Formats

Use @format to control rendering. Common values:

FormatUse For
tableArray of objects
listStyled list with {@title name, @subtitle email}
markdownRich text, diagrams
chart:bar / chart:line / chart:pieData visualization
jsonRaw JSON
dashboardComposite panels (auto-detected)

For complete format reference with layout hints, containers, and auto-detection rules, see references/output-formats.md.

Lifecycle & Hot-Reload

// Lifecycle hooks — receive optional context for hot-reload support
async onInitialize(ctx?: { reason?: string; oldInstance?: any }) {
  if (ctx?.reason === 'hot-reload' && ctx.oldInstance) {
    // Transfer non-serializable resources (sockets, timers, connections)
    this.socket = ctx.oldInstance.socket;
    ctx.oldInstance.socket = null; // prevent old instance from using it
    return;
  }
  // Normal first-time initialization
  this.socket = await createConnection();
}

async onShutdown(ctx?: { reason?: string }) {
  if (ctx?.reason === 'hot-reload') {
    return; // DON'T close resources — new instance will take them
  }
  // Real shutdown: clean up everything
  this.socket?.close();
}

Hot-reload rules:

  • onShutdown({ reason: 'hot-reload' }) → skip resource cleanup
  • onInitialize({ reason: 'hot-reload', oldInstance }) → transfer resources from old instance
  • Normal shutdown/init (no context) → full cleanup/setup
  • Backward compatible — photons without context param still work

Events & Channels

// Simple emit (local only — goes to current caller's UI)
this.emit({ status: 'processing', progress: 50 });

// Channel emit (cross-photon pub/sub via daemon broker)
// Framework auto-prefixes with photon name: 'messages' → 'whatsapp:messages'
this.emit({ channel: 'messages', type: 'message', data: msg });

// Subscribe to another photon's events (use the full prefixed name)
const broker = getBroker();
const sub = await broker.subscribe('whatsapp:messages', (msg) => {
  // handle message
});
sub.unsubscribe(); // when done

Channel naming: Emit with simple names (channel: 'messages'). The framework auto-prefixes with the photon name. Channels with a colon are left as-is.

Generator Workflows

// Multi-step with user interaction
async *deploy({ env }: { env: string }) {
  yield { emit: 'status', message: 'Deploying...' };
  const ok = yield { ask: 'confirm', message: `Deploy to ${env}?` };
  if (!ok) return 'Cancelled';
  return 'Done';
}

// Waiting for external async events (WebSocket, library callbacks, etc.)
async *connect() {
  yield { emit: 'status', message: 'Connecting...' };
  let resolve: (v: any) => void;
  const promise = new Promise(r => { resolve = r; });
  this.socket.on('ready', (data) => resolve(data));
  await this.initSocket();
  const event = await promise;  // blocks until external event fires
  yield { emit: 'toast', message: 'Connected!', type: 'success' };
  return event;
}

Scoped Memory

Zero-config persistent storage via this.memory:

await this.memory.set('key', value);              // photon scope (default)
const val = await this.memory.get<T>('key');
await this.memory.set('shared', data, 'global');   // cross-photon

Three scopes: photon (private), session (per-user), global (shared). Full API: get, set, delete, has, keys, clear, getAll, update.

Runtime Scheduling

Dynamic task scheduling via this.schedule — complements static @scheduled/@cron tags:

// Create a scheduled task at runtime
await this.schedule.create({
  name: 'nightly-cleanup',
  schedule: '0 0 * * *',       // 5-field cron or @daily, @hourly, @weekly, @monthly
  method: 'purge',
  params: { olderThan: 30 },
});

// Manage tasks
await this.schedule.pause(id);
await this.schedule.resume(id);
await this.schedule.cancel(id);
const tasks = await this.schedule.list('active');

Full API: create, get, getByName, list, update, pause, resume, cancel, cancelByName, cancelAll, has. Tasks persist to disk; the daemon executes them.

Use @scheduled/@cron for fixed schedules known at build time. Use this.schedule for dynamic schedules created at runtime (user-configured intervals, conditional jobs, etc.).

User Settings

Expose configurable options via protected settings. Runtime auto-generates a settings tool and persists to disk:

export default class MyAgent extends Photon {
  protected settings = {
    /** Polling interval in ms */
    pollIntervalMs: 5000,
    /** Max concurrent operations */
    maxConcurrent: 3,
    /** Auto-resume after restart */
    autoResume: true,
  };

  async doWork() {
    const interval = this.settings.pollIntervalMs; // read-only Proxy
  }
}

Users change settings via CLI (photon cli my-agent settings). Values persist to ~/.photon/state/<name>/<instance>-settings.json.

Live Rendering (v1.14+)

this.render(format, value) pushes formatted output that replaces the previous render zone (instead of appending):

export default class Monitor {
  async status() {
    while (true) {
      const metrics = await this.collectMetrics();
      this.render('table', metrics);  // Replaces previous output
      await new Promise(r => setTimeout(r, 5000));
    }
  }
}

Accepts the same format values as @format tags. Call this.render() with no arguments to clear the render zone.

Compile to Binary (v1.13+)

Build standalone executables from any photon — no Node.js required on the target machine:

photon build my-tool                         # Binary for current platform
photon build my-tool -t bun-linux-x64        # Cross-compile for Linux
photon build my-tool --with-app              # Embed Beam UI as a desktop app

Uses Bun's compiler. The binary bundles the photon, its @dependencies, and transitive @photon deps.

Install from GitHub (v1.14+)

Use qualified refs to install and run photons directly from any GitHub repository:

photon beam Arul-/photons/claw        # Install from GitHub, open in Beam
photon cli Arul-/photons/todo add     # Install from GitHub, run method

Format: owner/repo/photon-name. Transitive @photon dependencies from the same repo are resolved automatically.

Custom UIs

Link HTML files as interactive result renderers:

/** @ui dashboard ./ui/dashboard.html */
export default class MyApp extends Photon {
  /** @ui dashboard */
  async getData({ range }: { range: string }) { return { metrics: 42 }; }
}

The UI gets a photon-named global proxy: window.myApp.getData(...), window.myApp.onResult(...).

For full MCP Apps guide, see references/mcp-apps.md.

Directory Structure

Photon separates source/assets from runtime data under ~/.photon/:

~/.photon/
├── <name>.photon.ts        # Source (or symlink)
├── <name>/                 # Assets (@ui templates, images)
├── state/<name>/           # Runtime: this.memory, settings (automatic)
├── data/<name>/            # Runtime: photon-written files (manual)
├── cache/                  # Runtime: compiled .mjs cache
└── logs/<name>/            # Runtime: execution logs

Key rule: Photons that write runtime files (auth tokens, downloaded media, databases) MUST use ~/.photon/data/<name>/, never ~/.photon/<name>/. The asset folder is watched for hot-reload — writing there causes reload loops.

// Correct: runtime data goes in data/<name>/
const dataDir = path.join(os.homedir(), '.photon', 'data', 'my-app');

// Wrong: asset folder triggers hot-reload!
const dataDir = path.join(os.homedir(), '.photon', 'my-app', 'downloads');

For the full convention, see Directory Structure.

References

TopicWhen to Read
Directory StructureNeed the ~/.photon/ layout rules for assets vs runtime data
Docblock TagsNeed the complete tag reference (class, method, inline, daemon, MCP)
Output FormatsNeed layout hints, chart mapping, containers, or auto-detection rules
Dependency InjectionUsing @mcp, @photon, or this.call() for cross-photon communication
Daemon FeaturesSetting up webhooks, cron jobs, or distributed locks
User Settingsprotected settings, persistence, auto-resume patterns
MCP AppsBuilding custom HTML UIs with the photon bridge
VisualizationGenerating Mermaid diagrams from photons
Mermaid SyntaxFlowchart shapes, arrows, subgraphs
Photon PatternsCommon emit/ask/yield patterns with Mermaid equivalents
ExamplesComplete Photon-to-Mermaid conversion examples

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.

Repository SourceNeeds Review
169.7K96Kanthropics
Coding

remotion-best-practices

Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.

Repository SourceNeeds Review
153.7K2.2Kremotion-dev
Coding

azure-ai

Service Use When MCP Tools CLI

Repository SourceNeeds Review
139.2K157microsoft
Coding

azure-deploy

AUTHORITATIVE GUIDANCE — MANDATORY COMPLIANCE

Repository SourceNeeds Review
138.8K157microsoft