# Photon MCP Developer Guide

> **New to Photon?** Start with [Getting Started](getting-started.md) instead — you'll be running your first photon in 5 minutes. This page is the comprehensive reference for when you need to look something up.

Complete guide to creating `.photon.ts` files and understanding how Photon works.

## Table of Contents

1. [Quick Start](#quick-start)
2. [Creating Your First MCP](#creating-your-first-mcp)
3. [Settings: User-Configurable Knobs](#settings-user-configurable-knobs)
4. [Constructor Configuration (for secrets)](#constructor-configuration-for-secrets)
5. [Writing Tool Methods](#writing-tool-methods)
6. [Docblock Tags](#docblock-tags)
7. [Return Formatting](#return-formatting)
8. [Dependency Injection](#dependency-injection)
9. [Assets and UI](#assets-and-ui)
10. [Advanced Workflows](#advanced-workflows)
11. [Lifecycle Hooks](#lifecycle-hooks)
12. [Configuration Convention](#configuration-convention)
13. [Reactive Collections](#reactive-collections)
14. [Real-Time Sync](#real-time-sync)
15. [Sampling: `this.sample()`](#sampling-thissample)
16. [Scheduling: `@scheduled`, `this.schedule`, `photon ps`](#scheduling-scheduled-thisschedule-photon-ps)
17. [Common Patterns](#common-patterns)
18. [CLI Command Reference](#cli-command-reference)
19. [Testing and Development](#testing-and-development)
20. [Deployment](#deployment)
21. [How Photon Works](#how-photon-works)
22. [Best Practices](#best-practices)
23. [Troubleshooting](#troubleshooting)

---

## Quick Start



The fastest way to use Photon is via the **Beam**, a visual dashboard for managing your MCPs.

### 1. Install & Launch
Install the global package and run the `photon` command to open the dashboard in the web browser:

```bash
bun add -g @portel/photon
photon
```

### 2. Create an MCP
Ready to code? Create a new tool in seconds:

```bash
# 1. Generate template
photon maker new my-tool

# 2. Edit ~/.photon/my-tool.photon.ts
export default class MyTool {
  async greet(params: { name: string }) {
    return `Hello, ${params.name}!`;
  }
}

# 3. Run in dev mode
photon mcp my-tool --dev
```

That's it! Your MCP is now running and ready to use.

---

## Creating Your First MCP

### File Structure

A Photon MCP is a **single TypeScript file** with this minimal structure:

```typescript
export default class MyMCP {
  async toolName(params: { input: string }) {
    return `Result: ${params.input}`;
  }
}
```

### Naming Conventions

The MCP name comes from:
1. **File name** (preferred): `calculator.photon.ts` → `calculator`
2. **Class name** (fallback): `class Calculator` → `calculator`

### Complete Example

Here's a real-world example with all features:

```typescript
/**
 * Calculator - Basic arithmetic operations
 *
 * Provides mathematical calculations: add, subtract, multiply, divide.
 * Useful for numerical computations and data processing.
 *
 * Dependencies: None
 *
 * @version 1.0.0
 * @author Your Name
 */

export default class Calculator {
  // Optional lifecycle hook
  async onInitialize() {
    console.error('Ready to calculate');
  }

  /**
   * Add two numbers together
   * @param a First number
   * @param b Second number
   */
  async add(params: { a: number; b: number }) {
    return params.a + params.b;
  }

  /**
   * Subtract b from a
   * @param a First number
   * @param b Number to subtract
   */
  async subtract(params: { a: number; b: number }) {
    return params.a - params.b;
  }

  // Private helper (not exposed as tool)
  private _validate(value: number) {
    if (isNaN(value)) throw new Error('Invalid number');
  }
}
```

---

## Settings: User-Configurable Knobs

Whenever a value should be runtime-configurable, declare it on `protected settings`. Photon reads the property's JSDoc, generates an MCP `settings` tool with a typed input for each knob, and persists user changes to disk. Inside the photon, `this.settings` is a read-only Proxy.

**This is the right pattern in almost every case.** Reach for it first. Use the constructor pattern (next section) only when the value is a primitive secret that belongs in `.env` and never changes at runtime.

### Basic Pattern

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

export default class Poller extends Photon {
  /** User-tunable knobs */
  protected settings = {
    /** Polling interval in seconds */
    intervalSec: 60,
    /** Endpoint to poll */
    endpoint: 'https://api.example.com/status',
    /** Pause polling without unloading the photon */
    paused: false,
  };

  async tick() {
    if (this.settings.paused) return { skipped: true };
    return await fetch(this.settings.endpoint);
  }
}
```

What you get for free:
- An MCP tool named `settings` that lists current values, accepts updates, and validates types from the property declarations.
- JSDoc on each property becomes the tool's parameter description.
- Persistence to `~/.photon/state/<photon>/<instance>-settings.json`. Persisted values win over the in-source defaults on the next load.
- A writable Proxy on `this.settings`. You can read and write via `this.settings.key = value` from inside a photon method — the runtime persists the change and emits `settings:changed`, identical to what the `settings` MCP tool produces.

### Changing settings

From the CLI:
```bash
photon cli poller settings --intervalSec 30 --paused true
```

From an MCP client (Claude, Cursor, Beam): call the auto-generated `settings` tool with a partial object:
```json
{ "intervalSec": 30, "paused": true }
```

### When to use settings vs constructor

| Use `protected settings` for | Use the constructor for |
|---|---|
| Anything the user should change at runtime | Primitive secrets that belong in `.env` |
| Polling intervals, thresholds, modes, paths | API keys, tokens, passwords |
| Feature toggles, retry counts, defaults | Service URLs that never change between deploys |
| Any value with a sensible default | Required boot-time configuration |

If you find yourself writing `process.env.SOMETHING` in a method body, that is almost always a settings property in disguise — declare it on `protected settings` and Photon will surface it everywhere.

---

## Constructor Configuration (for secrets)

Constructor parameters are the right tool when a value is a primitive secret that should be supplied by the environment and never exposed in the runtime UI. Photon captures the resolved constructor values it injects and stores them under the owning `PHOTON_DIR`, so daemon-hosted photons can be recreated after daemon restarts. For everything else, prefer [Settings](#settings-user-configurable-knobs).

### Basic Pattern

Constructor parameters automatically map to **environment variables**:

```typescript
export default class Filesystem {
  constructor(
    private workdir: string = join(homedir(), 'Documents'),
    private maxFileSize: number = 10485760,
    private allowHidden: boolean = false
  ) {
    // Validate configuration
    if (!existsSync(workdir)) {
      throw new Error(`Working directory does not exist: ${workdir}`);
    }
  }
}
```

### Environment Variable Mapping

Pattern: `{MCP_NAME}_{PARAM_NAME}` in SCREAMING_SNAKE_CASE

| Constructor Parameter | Environment Variable |
|-----------------------|----------------------|
| `workdir` | `FILESYSTEM_WORKDIR` |
| `maxFileSize` | `FILESYSTEM_MAX_FILE_SIZE` |
| `allowHidden` | `FILESYSTEM_ALLOW_HIDDEN` |

Set these values normally in the environment before first loading the photon:

```bash
export FILESYSTEM_WORKDIR=/Users/me/Documents
photon beam
```

When Photon injects that constructor value, it persists the resolved value to `.data/...` for the current `PHOTON_DIR` and photon namespace. If the daemon restarts later without the original shell environment, constructor injection replays the persisted value. `photon config set` remains available as a manual repair or override command, but it is not the normal setup path.

Scheduled methods can declare config that must exist before the daemon arms the schedule:

```typescript
/**
 * Send the daily reminder.
 * @scheduled 0 9 * * *
 * @requiresConfig KITH_USER_EMAIL
 */
async remind() {
  const email = this.config.require('KITH_USER_EMAIL');
}
```

If `KITH_USER_EMAIL` is missing from Photon config, Photon refuses to enable the schedule and logs the missing key.

### Type Conversion

Photon automatically converts environment variable strings:

```typescript
constructor(
  private port: number = 3000,          // "8080" → 8080
  private enabled: boolean = false,     // "true" → true
  private tags: string[] = [],          // Not supported yet
) {}
```

**Supported types:**
- `string` - No conversion
- `number` - Parsed with `Number()`
- `boolean` - "true"/"1" → `true`, "false"/"0" → `false`

### Documentation

To provide descriptions for these parameters in the CLI and MCP help, use a `Configuration:` section in your class-level JSDoc:

```typescript
/**
 * Filesystem MCP
 * 
 * Configuration:
 * - workdir: Path to the working directory
 * - maxFileSize: Maximum file size in bytes
 */
export default class Filesystem {
  constructor(private workdir: string, private maxFileSize: number) {}
}
```

> [!NOTE]
> Arrays (`string[]`, etc.) are not yet supported for direct environment variable mapping in the constructor. Use interactive elicitation in tool methods for complex user input.

### Smart Defaults

Use platform-aware defaults:

```typescript
import { homedir } from 'os';
import { join } from 'path';

constructor(
  // Cross-platform Documents folder
  private workdir: string = join(homedir(), 'Documents'),

  // Reasonable file size limit (10MB)
  private maxFileSize: number = 10485760,

  // Conservative security default
  private allowHidden: boolean = false
) {}
```

### Required Parameters

For required config, omit defaults and throw clear errors:

```typescript
constructor(
  private apiKey: string,
  private endpoint: string
) {
  if (!apiKey || !endpoint) {
    throw new Error('API key and endpoint are required');
  }
}
```

**User experience:** When users run `photon my-tool --config`, they see:

```json
{
  "env": {
    "MY_TOOL_API_KEY": "<your-api-key>",
    "MY_TOOL_ENDPOINT": "<your-endpoint>"
  }
}
```

### Configuration Examples

**API Client:**
```typescript
constructor(
  private baseUrl: string = 'https://api.example.com',
  private timeout: number = 5000,
  private apiKey?: string  // Optional authentication
) {}
```

**Database:**
```typescript
constructor(
  private dbPath: string = join(homedir(), '.myapp', 'data.db'),
  private readonly: boolean = false
) {
  if (!existsSync(dirname(dbPath))) {
    mkdirSync(dirname(dbPath), { recursive: true });
  }
}
```

**Git Operations:**
```typescript
constructor(
  private repoPath: string = process.cwd(),
  private autoCommit: boolean = false
) {
  if (!existsSync(join(repoPath, '.git'))) {
    throw new Error(`Not a git repository: ${repoPath}`);
  }
}
```

---

## Writing Tool Methods

### Method Signature

Every tool is an **async method** with a single `params` object:

```typescript
async methodName(params: {
  requiredParam: string;
  optionalParam?: number;
  arrayParam?: string[];
  objectParam: {
    nested: boolean;
  };
}) {
  return result;
}
```

### JSDoc Documentation

JSDoc comments become tool descriptions in MCP:

```typescript
/**
 * Read file contents from the filesystem
 * @param path Path to file (relative to working directory)
 * @param encoding File encoding (default: utf-8)
 */
async read(params: { path: string; encoding?: string }) {
  // Implementation
}
```

**What MCP clients see:**
- Tool name: `read`
- Description: "Read file contents from the filesystem"
- Parameters:
  - `path` (required): "Path to file (relative to working directory)"
  - `encoding` (optional): "File encoding (default: utf-8)"

### Return Values

Photon accepts multiple return formats:

```typescript
// 1. Simple value (string, number, boolean)
async tool1(params: {}) {
  return "Success";
}

// 2. Object (auto-stringified to JSON)
async tool2(params: {}) {
  return { result: 42, status: "ok" };
}

// 3. Success/error format (recommended)
async tool3(params: {}) {
  try {
    // Do work
    return { success: true, result: data };
  } catch (error: any) {
    return { success: false, error: error.message };
  }
}

// 4. MCP content format
async tool4(params: {}) {
  return {
    content: [
      { type: "text", text: "Result data" }
    ]
  };
}
```

### Error Handling

Handle errors gracefully:

```typescript
async readFile(params: { path: string }) {
  try {
    // Validate input
    if (!params.path) {
      return { success: false, error: 'Path is required' };
    }

    // Resolve path safely
    const fullPath = this._resolvePath(params.path);

    // Perform operation
    const content = await readFile(fullPath, 'utf-8');

    return {
      success: true,
      content,
      path: fullPath
    };
  } catch (error: any) {
    return {
      success: false,
      error: error.message
    };
  }
}
```

### TypeScript Type Support

Photon extracts JSON schemas from TypeScript types:

```typescript
async process(params: {
  // Primitives
  name: string;
  age: number;
  active: boolean;

  // Optional
  nickname?: string;

  // Arrays
  tags: string[];
  scores?: number[];

  // Objects
  settings: {
    theme: string;
    notifications: boolean;
  };

  // Union types (as strings in schema)
  status: 'active' | 'inactive' | 'pending';
}) {
  return { processed: true };
}
```

**Current limitations:**
- No support for complex union types beyond string literals
- No support for generics or mapped types
- Use interfaces/types for complex nested objects

### Private Methods

Methods starting with `_` or marked `private` are **not exposed as tools**:

```typescript
export default class MyMCP {
  // Public tool
  async publicMethod(params: { input: string }) {
    return this._helper(params.input);
  }

  // Private helper (NOT a tool)
  private _helper(input: string) {
    return input.toUpperCase();
  }

  // Also private (NOT a tool)
  async _privateMethod() {
    return "Not exposed";
  }
}
```

---

## Docblock Tags

Photon uses JSDoc-style docblock tags to extract metadata, configure tools, and generate documentation.

### Class-Level Tags
Place these in the JSDoc comment at the top of your `.photon.ts` file.

| Tag | Usage | Example |
|---|---|---|
| `@version` | Photon version | `@version 1.0.0` |
| `@author` | Author name | `@author Jane Doe` |
| `@license` | License type | `@license MIT` |
| `@repository` | Source repository URL | `@repository github.com/user/repo` |
| `@homepage` | Project homepage | `@homepage example.com` |
| `@runtime` | **Required** runtime version range | `@runtime ^1.5.0` |
| `@dependencies` | NPM packages to auto-install | `@dependencies axios@^1.0.0` |
| `@mcp` | Inject MCP dependency | `@mcp github anthropics/mcp` |
| `@photon` | Inject Photon dependency | `@photon utils ./utils.photon.ts` |
| `@stateful` | Enable stateful mode | `@stateful true` |
| `@idleTimeout` | Idle timeout in ms | `@idleTimeout 300000` |
| `@ui` | Define UI template asset | `@ui main ./ui/index.html` |
| `@prompt` | Define prompt asset | `@prompt system ./prompts/sys.txt` |
| `@resource` | Define resource asset | `@resource data ./data.json` |

### Method-Level Tags
Place these immediately preceding a tool method.

| Tag | Usage | Example |
|---|---|---|
| `@param` | Describe parameter | `@param name User name` |
| `@returns` | Describe return value | `@returns The result` |
| `@example` | Provide usage example | `@example await tool.run()` |
| `@format` | Output format hint | `@format table` |
| `@icon` | Tool icon (emoji/name) | `@icon 🧮` |
| `@autorun` | Auto-run in UI | `@autorun` |
| `@ui` | Link to UI template | `@ui main` |

### Daemon Tags (Advanced)
Enable background features handled by the Photon Daemon.

| Tag | Usage | Example |
|---|---|---|
| `@webhook` | Expose as HTTP webhook | `@webhook stripe` |
| `@scheduled` | Cron schedule | `@scheduled 0 0 * * *` |
| `@locked` | Distributed lock | `@locked resource:write` |

### Inline Parameter Tags
Use these *within* `@param` descriptions for validation and UI generation.

| Tag | functionality | Example |
|---|---|---|
| `{@min N}` | Minimum value | `@param age {@min 18}` |
| `{@max N}` | Maximum value | `@param score {@max 100}` |
| `{@pattern R}` | Regex pattern | `@param code {@pattern ^[A-Z]{3}$}` |
| `{@choice A,B}` | Enum/Choice | `@param color {@choice red,blue}` |
| `{@field T}` | HTML input type | `@param bio {@field textarea}` |
| `{@format T}` | Data format | `@param email {@format email}` |
| `{@example V}` | Example value | `@param city {@example London}` |
| `{@label L}` | Custom UI label | `@param id {@label User ID}` |

---

## Return Formatting
 
 Photon allows hinting the data shape and type of return values using the `@format` tag. This helps the CLI and Web interfaces render the data optimally.
 
 ### Structural Formats
 Structural hints tell Photon how to organize the data table or tree.
 
 | Format | Description | Used For |
 |---|---|---|
 | `primitive` | Formats result as a single value | Strings, numbers, booleans |
 | `table` | Formats results as a grid | Arrays of objects |
 | `list` | Formats results as a bulleted list | Arrays of primitives |
 | `grid` | Formats results as a visual grid | Arrays of objects/images |
 | `card` | Formats result as a detailed card | Single object |
 | `tree` | Formats results as a hierarchy | Nested objects/JSON |
 | `none` | Raw JSON output | Complex data without specific shape |
 
 ### Content & Code Formats
 Content hints specify the syntax for text coloring and highlighting.
 
 - **Content Types**: `json`, `markdown`, `yaml`, `xml`, `html`, `mermaid`
 - **Code Blocks**: `code` (generic) or `code:language` (e.g., `code:typescript`)
 
 ### Advanced Layout Hints
 For `list`, `table`, and `grid` formats, you can specify layout hints using nested syntax:
 
 ```typescript
 /**
  * @format list {@title name, @subtitle email, @icon avatar}
  */
 ```
 
 | Hint | Description |
 |---|---|
 | `@title field` | Primary display text |
 | `@subtitle field` | Secondary display text |
 | `@icon field` | Leading icon/image |
 | `@badge field` | Status badge |
 | `@columns N` | Grid column count |
 | `@style S` | Style: `plain`, `grouped`, `inset` |
 
 **Example:**
 ```typescript
 /**
  * List files in directory
  * @format table
  */
 async ls(params: { path: string }) {
   return await this._listFiles(params.path);
 }
 
 /**
  * Get system report
  * @format markdown
  */
 async report() {
   return "# System Status\n- CPU: 10%\n- RAM: 4GB";
 }

 /**
  * Sales by region with formatted columns
  * @format table {@columnFormats revenue:currency,margin:percent,region:truncate(15)}
  */
 async sales() {
   return [
     { region: "North America", revenue: 142830, margin: 0.23 },
     { region: "Europe", revenue: 98450, margin: 0.18 },
   ];
 }

 /**
  * Skill assessment
  * @format chart:radar
  */
 async skills() {
   return [{ name: "Alice", communication: 8, coding: 9, design: 6, leadership: 7, testing: 8 }];
 }

 /**
  * Upload progress
  * @format ring
  */
 async progress() {
   return { value: 73, max: 100, label: "Upload" };
 }
 ```

---

## Dependency Injection

Photon uses constructor parameters as the single injection point for all dependencies: environment variables, MCP clients, photon instances, and persisted state. See [Constructor Injection](core/CONSTRUCTOR-INJECTION.md) for the complete reference.

### Declaring Dependencies
Use `@mcp` and `@photon` tags at the class level to declare external dependencies.

```typescript
/**
 * @mcp github anthropics/mcp-server-github
 * @mcp storage filesystem
 */
export default class Manager {
  constructor(
    private apiKey: string,  // Env var: MANAGER_API_KEY
    private github: any,     // Injected from @mcp github
    private storage: any     // Injected from @mcp storage
  ) {}
}
```

### Injection Rules
- **Primitive parameters** (`string`, `number`, `boolean`) → mapped to environment variables
- **Non-primitive parameters** matching an `@mcp` declaration → MCP client proxy
- **Non-primitive parameters** matching a `@photon` declaration → loaded photon instance
- **Non-primitive parameters** with defaults on `@stateful` photons → restored from persisted state

---

## Assets and UI

Photon supports "MCP Apps" by allowing you to bundle UI templates, prompts, and static resources directly with your Photon server.

### Declaring Assets
Use `@ui`, `@prompt`, and `@resource` tags at the class level to link local files as assets.

```typescript
/**
 * @ui dashboard
 * @prompt welcome ./prompts/welcome-message.txt
 * @resource data ./assets/data.json
 */
export default class MyApp {
  /**
   * Show the main dashboard
   * @ui dashboard
   */
  async showDashboard() {
    return { success: true };
  }
}
```

Pathless `@ui dashboard` resolves by convention from the photon UI folder:

1. `ui/dashboard.photon.tsx`
2. `ui/dashboard.tsx`
3. `ui/dashboard.photon.html`
4. `ui/dashboard.html`

Use the explicit path form only when the file lives outside the convention,
for example `@ui dashboard ./dashboard/dist/index.html` for a prebuilt app.
When the resolved UI is `.tsx`, Photon treats it as the client application
shell in Beam: runtime paths such as `/mcp` and declared web routes still win,
and otherwise GET routes fall through to the TSX app for client-side routing.

### Linking UI to Tools
Use the method-level `@ui` tag to specify which UI template should be rendered when a tool is invoked in a compatible interface (like the Photon Playground or a custom web UI).

### MCP Apps Compatibility (SEP-1865)

Photon implements the **MCP Apps Extension (SEP-1865)**, the official standard for interactive UIs in MCP. This means:

1. **`ui://` Resource URIs**: Tools with linked UIs expose `_meta.ui.resourceUri` pointing to `ui://photon-name/ui-id`
2. **JSON-RPC Protocol**: UI iframes communicate via standard `ui/initialize`, `ui/ready`, and `tools/call` messages
3. **Cross-Platform Support**: UIs built for Claude, ChatGPT, or any MCP Apps-compatible host work in Photon

#### Protocol Messages

| Message | Direction | Purpose |
|---------|-----------|---------|
| `ui/initialize` | Host → App | Initialize with theme, capabilities, dimensions |
| `ui/ready` | App → Host | App is ready to receive data |
| `tools/call` | App → Host | Request tool execution from the UI |
| `ui/notifications/tool-result` | Host → App | Push tool result to UI |

#### Client APIs

Photon injects APIs into UI iframes for maximum compatibility:

```javascript
// 1. Photon global — named after your .photon.ts file (recommended)
// For search.photon.ts:
search.onResult(data => console.log(data));
search.query({ q: 'test' });  // calls the 'query' method

// 2. Low-level bridge — full control over tool I/O
photon.callTool('query', { q: 'test' });
photon.onResult(data => console.log(data));
photon.theme; // 'light' | 'dark'

// 3. OpenAI Apps SDK compatible
openai.callTool('query', { q: 'test' });
```

#### Building Compatible UIs

UIs that work with the official MCP Apps SDK (`@modelcontextprotocol/ext-apps`) will work in Photon without modification:

```typescript
// Using official MCP Apps SDK
import { App } from '@modelcontextprotocol/ext-apps';

const app = new App({ name: 'My App', version: '1.0.0' });
app.connect();
app.ontoolresult = (result) => {
  // Handle result
};
```

Or use Photon's native API:

```javascript
// Using the photon global (named after your .photon.ts file)
// For myTool.photon.ts:
myTool.onResult(result => {
  document.getElementById('output').textContent = result;
});
```

#### Resources

- [MCP Apps Specification](https://modelcontextprotocol.io/docs/extensions/apps)
- [SEP-1865 GitHub](https://github.com/modelcontextprotocol/ext-apps)
- [Platform Compatibility Source](./src/auto-ui/platform-compat.ts)

---

## Advanced Workflows

Photon supports interactive and stateful workflows using `async` generators and the `ask`/`emit` pattern.

### Interactive Tools (ask/emit)
Use the `ask`/`emit` pattern to create interactive CLI tools or conversational MCPs.

```typescript
export default class InteractiveTool {
  async *survey() {
    // Emit progress
    yield { emit: 'progress', value: 0.2, message: 'Starting survey...' };

    // Ask for text
    const name = yield { ask: 'text', message: 'What is your name?' };

    // Ask for confirmation
    const confirm = yield { ask: 'confirm', message: `Is ${name} correct?` };

    if (!confirm) return "Aborted";

    // Ask for selection
    const color = yield { 
      ask: 'select', 
      message: 'Favorite color?', 
      options: ['Red', 'Green', 'Blue'] 
    };

    yield { emit: 'progress', value: 1.0, message: 'Done!' };
    return `Name: ${name}, Favorite Color: ${color}`;
  }
}
```

### Stateful Workflows
Mark a class as `@stateful` and use `checkpoint` yields to persist state across sessions. This is ideal for long-running workflows or tasks that require manual approval.

```typescript
/**
 * @stateful true
 */
export default class Workflow {
  async *execute(params: { task: string }) {
    console.error("Starting task:", params.task);

    // Initial work
    const step1 = await someAsyncWork();

    // Persist state here. If process restarts, it resumes from here.
    yield { checkpoint: 'step1_complete', data: { step1 } };

    // Next step
    const step2 = await nextWork(step1);

    return { step1, step2 };
  }
}
```

### Stateful Persistence via Constructor

For simpler stateful photons that don't need generators or checkpoints, `@stateful true` combined with constructor defaults gives you automatic persistence across daemon restarts:

```typescript
/**
 * @stateful true
 */
export default class List {
  items: string[];
  constructor(items: string[] = []) {
    this.items = items;
  }

  add(item: string): void {
    this.items.push(item);
    // Reactive array auto-persists state to disk
  }

  getAll(): string[] {
    return this.items;
  }
}
```

The runtime snapshots non-primitive constructor parameters on every mutation and restores them via constructor injection on restart. See [Constructor Injection](core/CONSTRUCTOR-INJECTION.md) for the full design.

---

## Lifecycle Hooks

Photon supports two optional lifecycle hooks:

### onInitialize

Called once when the MCP server starts:

```typescript
async onInitialize() {
  console.error('Starting up...');
  console.error(`Working directory: ${this.workdir}`);

  // Initialize resources
  await this._connectDatabase();
  await this._loadConfig();

  console.error('✅ Ready');
}
```

**Use cases:**
- Log configuration
- Validate environment
- Initialize connections
- Load resources

### onShutdown

Called when the MCP server is shutting down:

```typescript
async onShutdown() {
  console.error('Shutting down...');

  // Clean up resources
  await this.db?.close();
  await this.httpClient?.dispose();

  console.error('✅ Shutdown complete');
}
```

**Use cases:**
- Close database connections
- Clean up temp files
- Flush caches
- Save state

### Complete Example

```typescript
import Database from 'better-sqlite3';

export default class SqliteMCP {
  private db?: Database.Database;

  constructor(private dbPath: string = join(homedir(), 'data.db')) {}

  async onInitialize() {
    console.error(`Opening database: ${this.dbPath}`);
    this.db = new Database(this.dbPath);
    console.error('✅ Database ready');
  }

  async onShutdown() {
    console.error('Closing database...');
    this.db?.close();
    console.error('✅ Database closed');
  }

  async query(params: { sql: string }) {
    if (!this.db) throw new Error('Database not initialized');
    const result = this.db.prepare(params.sql).all();
    return { success: true, rows: result };
  }
}
```

---

## Configuration Convention

> **Prefer [`protected settings`](#settings-user-configurable-knobs) for new photons.** It auto-generates a typed `settings` MCP tool, persists changes to disk, and exposes a read-only Proxy on `this.settings` — no `configure()` plumbing required. The `configure()` pattern below is kept for compatibility with photons written before the settings system existed.

The `configure()` method is a by-convention pattern for photon configuration. Similar to how `main()` makes a photon a UI application, `configure()` makes it a configurable photon.

### Why Use configure()?

When a photon needs persistent settings that:
- Are shared across all instances (MCP, Beam UI, CLI)
- Should be collected once during install/setup
- Need to work alongside environment variables

### Basic Pattern

```typescript
import { PhotonMCP, loadPhotonConfig, savePhotonConfig } from '@portel/photon-core';

interface MyConfig {
  apiEndpoint: string;
  maxRetries?: number;
  enableCache?: boolean;
}

export default class MyService extends PhotonMCP {
  /**
   * Configure this photon
   *
   * Set your API endpoint and options. This is stored persistently
   * so all instances use the same configuration.
   */
  async configure(params: {
    /** The API endpoint URL */
    apiEndpoint: string;
    /** Max retry attempts (default: 3) */
    maxRetries?: number;
    /** Enable response caching */
    enableCache?: boolean;
  }): Promise<{ success: boolean; config: MyConfig }> {
    const config: MyConfig = {
      apiEndpoint: params.apiEndpoint,
      maxRetries: params.maxRetries ?? 3,
      enableCache: params.enableCache ?? true,
    };

    savePhotonConfig('my-service', config);
    return { success: true, config };
  }

  /**
   * Get current configuration
   */
  async getConfig(): Promise<MyConfig & { configPath: string }> {
    const config = loadPhotonConfig<MyConfig>('my-service', {
      apiEndpoint: '',
      maxRetries: 3,
      enableCache: true,
    });
    return {
      ...config,
      configPath: getPhotonConfigPath('my-service'),
    };
  }

  // Use config in your methods
  async fetchData() {
    const config = loadPhotonConfig<MyConfig>('my-service');
    const response = await fetch(config.apiEndpoint);
    // ...
  }
}
```

### How It Works

| Aspect | Description |
|--------|-------------|
| Storage | `~/.photon/{photonName}/config.json` |
| Scope | Shared across all instances |
| Detection | Schema extractor finds `configure()` method |
| Tools | Neither `configure()` nor `getConfig()` appear as MCP tools |

### Configuration Utilities

```typescript
import {
  loadPhotonConfig,    // Load config with defaults
  savePhotonConfig,    // Save config
  hasPhotonConfig,     // Check if configured
  getPhotonConfigPath, // Get config file path
  deletePhotonConfig,  // Remove config
} from '@portel/photon-core';

// Load with defaults
const config = loadPhotonConfig('my-photon', { theme: 'dark' });

// Save config
savePhotonConfig('my-photon', { theme: 'light', fontSize: 14 });

// Check if configured
if (!hasPhotonConfig('my-photon')) {
  console.log('Please run configure() first');
}
```

### Combined Setup: Environment Variables + Configuration

During install or reconfigure, both are collected together:

```typescript
/**
 * My API Client
 *
 * @env API_KEY - Your secret API key
 */
export default class ApiClient extends PhotonMCP {
  constructor(private apiKey: string) {
    super();
  }

  /**
   * Configure additional settings
   */
  async configure(params: {
    /** API endpoint URL */
    endpoint: string;
    /** Request timeout in ms */
    timeout?: number;
  }) {
    savePhotonConfig('api-client', params);
    return { success: true };
  }
}
```

Setup flow:
1. User installs photon
2. Framework detects: `apiKey` (env var) + `endpoint`, `timeout` (config)
3. Single setup UI collects all values
4. Env vars saved to `.env`, config to `~/.photon/api-client/config.json`

### Convention Methods Summary

| Method | Purpose | Appears as Tool? |
|--------|---------|------------------|
| `main()` | UI entry point | ✓ Yes |
| `configure()` | Persistent configuration | ✗ No |
| `getConfig()` | Read configuration | ✗ No |
| `onInitialize()` | Startup lifecycle | ✗ No |
| `onShutdown()` | Shutdown lifecycle | ✗ No |

### Reconfiguration

Already-configured photons can be reconfigured at any time. In Beam, users click the config icon on an installed photon to edit its settings. On the CLI, run the configure method again.

When you reconfigure, new values are merged with existing ones. If your config had `{ endpoint: "https://old.api.com", timeout: 5000 }` and you only submit a new `endpoint`, the `timeout` stays put. No data lost, no surprises.

Under the hood, Beam calls `reloadFile` after saving the updated config, which recompiles the photon and picks up the new values without restarting the server. It is, genuinely, that simple.

---

## Reactive Collections

Photon can make your plain arrays reactive, which means mutations like `.push()` and `.splice()` automatically emit events. No decorators, no wrapper functions, no "please remember to call `notifyListeners()`." You just write normal TypeScript.

There are three levels, depending on how much you want.

### Truly Zero Effort

If your class extends `PhotonMCP` and has array properties, the compiler does everything:

```typescript
import { PhotonMCP } from '@portel/photon-core';

interface Task {
  id: string;
  text: string;
}

export default class TodoList extends PhotonMCP {
  items: Task[] = [];  // Completely normal TypeScript

  async add(params: { text: string }) {
    this.items.push({ id: crypto.randomUUID(), text: params.text });
    // Auto-emits 'items:added'. You wrote zero extra code.
  }

  async remove(params: { id: string }) {
    const idx = this.items.findIndex(t => t.id === params.id);
    if (idx !== -1) this.items.splice(idx, 1);
    // Auto-emits 'items:removed'
  }
}
```

The compiler detects `extends PhotonMCP` plus array properties, and auto-injects the reactive wiring at build time. Your source stays clean.

### Level 1: Explicit Import

If you prefer being explicit about what is happening (or your class does not extend PhotonMCP), import `Array` from photon-core. It shadows the global `Array` with a reactive version:

```typescript
import { Array } from '@portel/photon-core';

interface Task {
  id: string;
  text: string;
}

export default class TodoList {
  items: Array<Task> = [];

  async add(params: { text: string }) {
    this.items.push({ id: crypto.randomUUID(), text: params.text });
    // Auto-emits 'items:added'
  }
}
```

Same result, just more visible. The `= []` initializer gets transformed to `= new Array()` at compile time.

### Level 2: Rich Collections

For cases where you need query methods (filtering, sorting, grouping), use `Collection<T>`. Think of it as a reactive array that also happens to have opinions about data access:

```typescript
import { Collection } from '@portel/photon-core';

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  category: string;
}

export default class ProductCatalog extends PhotonMCP {
  products = new Collection<Product>();

  async inStock() {
    return this.products.where('stock', '>', 0);
  }

  async byCategory(params: { category: string }) {
    return this.products
      .where('category', params.category)
      .sortBy('price');
  }

  async summary() {
    return {
      total: this.products.count(),
      categories: this.products.groupBy('category'),
      avgPrice: this.products.avg('price'),
    };
  }
}
```

Collections give you convenience methods like `.where()`, `.pluck()`, `.groupBy()`, `.sortBy()`, `.avg()`, and `.count()`. They also provide rendering hints for auto-UI (tables, cards, charts). And they are still reactive, so mutations emit events just like plain arrays.

### How It Works

The magic is split between compile time and runtime:

1. **Compile time**: The Photon compiler sees `= []` on class properties and transforms it to `= new Array()` (the reactive version, not the global one). For `extends PhotonMCP` classes, the import is auto-injected.

2. **Runtime**: After the class is instantiated, the loader inspects instance properties. For any `ReactiveArray`, it auto-wires:
   - `_propertyName` to the property key (so events know their source)
   - `_emitter` bound to `instance.emit.bind(instance)` (so events flow through the photon's event system)

3. **You**: Write `this.items.push(thing)` and move on with your life.

---

## Real-Time Sync

Photon methods can push updates to all connected clients in real time. This is how a kanban board updated in Claude Desktop also updates in Beam, or how a long-running task can stream progress without anyone polling.

### Emitting Events

Any method can call `this.emit()` to broadcast data:

```typescript
export default class KanbanBoard extends PhotonMCP {
  async move(params: { taskId: string; column: string }) {
    await this.updateTask(params.taskId, { column: params.column });

    // Push update to every connected client
    this.emit('board:updated', {
      taskId: params.taskId,
      column: params.column,
    });

    return { success: true };
  }
}
```

### Event Flow

The path from `this.emit()` to a browser tab is short but crosses a few boundaries:

```
this.emit('board:updated', data)
       │
       ▼
  Daemon pub/sub (Unix socket)
       │
       ▼
  SSE push to all connected clients
       │
       ▼
  Beam tab, Claude Desktop, other Beam tabs...
```

Every client subscribed to that photon's channel gets the event. If you have Beam open in two browser tabs and Claude Desktop running, all three update simultaneously.

### Live Rendering with `this.render()`

For methods that produce incremental output (progress dashboards, streaming data, live metrics), use `this.render()` to push formatted content that replaces the previous output rather than appending:

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

In the CLI, `this.render()` manages a dedicated render zone that clears and repaints on each call. In Beam, it updates the result area in place. Call `this.render()` with no arguments to clear the render zone.

### Runtime Helpers (UI Feedback Events)

Both async methods and generator methods can push transient UI events. Helpers on `this` are 1:1 wrappers around `yield { emit: ... }` so the two styles produce identical wire events — pick whichever reads better:

| Helper | Generator equivalent | Effect |
|--------|---------------------|--------|
| `this.toast(msg, { type?, duration? })` | `yield { emit: 'toast', message, type, duration }` | Transient notification bubble (Beam toast-manager, CLI prefixed log) |
| `this.status(msg)` | `yield { emit: 'status', message }` | Ephemeral spinner / progress message without a value |
| `this.progress(value, msg?)` | `yield { emit: 'progress', value, message }` | Progress bar (0..1 or 0..100) |
| `this.log(msg, { level?, data? })` | `yield { emit: 'log', message, level, data }` | Structured log entry |
| `this.thinking(active?)` | `yield { emit: 'thinking', active }` | Indeterminate "thinking" indicator |
| `this.render(format, value)` | `yield { emit: 'render', format, value }` | Replace the live render zone with a formatted value |
| `this.render()` | `yield { emit: 'render:clear' }` | Clear the render zone |

`render('toast' \| 'status' \| 'progress', value)` is sugar that routes through the dedicated emit event, so `this.render('toast', 'Saved!')` and `this.toast('Saved!')` are equivalent.

All of these are auto-injected on plain classes when the runtime detects their usage — no base class required. When extending the `Photon` base class they come from the mixin.

#### Custom formats from the server → custom UI

When you emit a format the auto-renderer doesn't recognize, Beam looks for a `@ui format-<name>` template. Inside that template you can reuse the auto-renderers via `photon.render(element, data, 'table'|'gauge'|...)` — see [Using Auto UI Renderers (photon.render)](./guides/CUSTOM-UI.md#using-auto-ui-renderers-photonrender) for the client-side API and the full format list.

### Receiving Events in Custom UIs

If your photon has a `@ui` template, use the auto-injected global named after your photon:

```javascript
// Inside your @ui HTML template for kanban.photon.ts
// Subscribe to specific events using on + PascalCase convention
kanban.onBoardUpdated(data => renderBoard(data));
kanban.onTaskMoved(data => animateTask(data));

// Or use the low-level bridge for raw event access
photon.onEmit(event => console.log(event.emit, event.data));
```

That is the entire client-side setup. The bridge handles SSE subscription, reconnection, and message parsing.

### Reactive Collections + Real-Time Sync

These two features compose naturally. When a reactive array emits `items:added`, that event flows through the same daemon pub/sub pipeline:

```typescript
export default class TodoList extends PhotonMCP {
  items: Task[] = [];

  async add(params: { text: string }) {
    this.items.push({ id: crypto.randomUUID(), text: params.text });
    // 'items:added' auto-emits → daemon → SSE → all clients
  }
}
```

No explicit `this.emit()` needed. The reactive array handles it.

---

## Sampling: `this.sample()`

`this.sample()` asks the **calling agent's model** to generate text on
your photon's behalf. No API key, no SDK, the agent that invoked your
photon pays for the tokens. Routes through MCP sampling on STDIO/Beam,
through the SSE response stream on Cloudflare Workers, and through a
hosted-LLM fallback on the local daemon if the client doesn't support
sampling.

```typescript
const summary = await this.sample({
  prompt: `Summarize this in one sentence:\n\n${article}`,
  maxTokens: 200,
});
```

Accepts `prompt` (single user turn) or `messages` (full multi-turn
history), plus the standard sampling knobs: `systemPrompt`,
`temperature`, `maxTokens`, `stopSequences`, `modelPreferences`,
`includeContext`. Returns the model's text output as a string.

### Augmenting `this.sample()` (v1.28.0+)

Three composable behaviors layer onto every `this.sample()` call without
changing the call site. They make agent loops self-correcting and let
photons accumulate persistent guidance.

**1. Memory include convention.** Memory keys with reserved prefixes
auto-inject into every sample call:

| Prefix              | Where it lands                                                      |
|---------------------|---------------------------------------------------------------------|
| `include_system_*`  | Prepended to `systemPrompt` (in addition to whatever the caller passed). |
| `include_transient_*`| Appended as a trailing user message after the conversation turn. |

Persistent guidance pattern:

```typescript
// Set once, e.g. during onInitialize:
await this.memory.set('include_system_voice', 'Always answer in plain English. No filler.');
await this.memory.set('include_system_format', 'Return JSON with keys: title, summary.');

// Now every this.sample() call inherits both system prompts:
const out = await this.sample({ prompt: userQuestion });
```

The convention is enforced by `src/sample-augmenter.ts` and the merged
prompts ride through to the underlying sampling provider unchanged.

> **Filename safety**: the FileMemoryBackend sanitizes key names —
> non-alphanumerics other than `_ . -` become `_`. Use underscores in
> the prefix (`include_system_voice`, not `include:system:voice`) so the
> stored filename and the prefix filter agree.

**2. Transient context registry: `this.context`.** Per-instance named
sections with priority, assembled into the trailing message alongside
memory transient includes.

```typescript
this.context.add('user_intent', 'wants a one-paragraph reply', 'high');
this.context.add('recent_history', historyBlock, 'medium');
this.context.add('debug_state',  diagnosticsBlock, 'low');

// Trailing context is assembled under a char budget (default 8000).
// Whole sections are dropped when over budget — never truncated mid-content.
// Drop order: low → medium → high.
```

Useful when the relevant context is computed per call (history, search
results, current task state) and you don't want to stringify it into
every prompt manually.

**3. Repeat-loop detection.** The augmenter tracks the last 8 normalized
responses per instance. On consecutive duplicates, it injects a graded
signal into the next call's `systemPrompt`:

| Consecutive duplicates | Signal injected            |
|------------------------|----------------------------|
| 1                      | `INFO: repeat detected, consider varying your output` |
| 2                      | `WARN: still repeating, change approach`              |
| 3+                     | `ERROR: stuck in a loop, abort the current path`      |

The signal is **inline feedback**, not a meta-comment — the model sees
it as system context for the next turn so it can self-correct.
`assembleSampleParams()` orders augmentations as: repeat signal →
memory system → caller's `systemPrompt` (the caller's prompt always
wins on conflicts).

All three compose. None require any setup beyond writing memory keys
and adding to `this.context`. To opt out of any single behavior, leave
its inputs empty — there's no flag.

---

## Scheduling: `@scheduled`, `this.schedule`, `photon ps`

Photon runs methods on a cron schedule via two surfaces:

- **`@scheduled` tag** in source — declares intent. The cron is part of the
  code, version-controlled, and travels with the photon.
- **`this.schedule.create()`** at runtime — for schedules whose cron, name,
  or method is dynamic (e.g. configured by the user through a UI).

Both are managed by the same daemon. `photon ps` is the operator surface.

### The two-step model: DECLARED → ENABLED → ACTIVE

A `@scheduled` annotation does **not** automatically fire its cron. The
daemon discovers the tag at boot and lists it as **DECLARED**. You enroll
it with `photon ps enable` and it moves to **ACTIVE**. This split exists
so a new annotation in source doesn't silently start running the moment
someone pulls main — enrollment is an explicit operator decision per
machine.

```
┌────────────────────────────────────────────────────────────────────┐
│  Source (@scheduled)  →  DECLARED  ──[photon ps enable]──→ ACTIVE  │
│                                          ↑                  │      │
│                                          └─[photon ps disable]     │
└────────────────────────────────────────────────────────────────────┘
```

`this.schedule.create()` skips DECLARED — it writes directly to the
active schedule list and the timer arms immediately.

### Declaring with `@scheduled`

Place the tag in the method's JSDoc. Five-field standard cron, plus the
nicknames `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`.

```typescript
export default class Newsletter {
  /**
   * Send the daily digest at 7:00 every morning.
   *
   * @scheduled 0 7 * * *
   */
  async sendDigest(): Promise<{ sent: number }> {
    // ... body runs once per cron tick
  }
}
```

After deploying the photon, enroll the schedule:

```bash
photon ps enable newsletter:sendDigest
# Enabled newsletter:sendDigest (0 7 * * *) under ~/Projects/my-base
```

The cron now fires from the daemon. Source edits to the cron string are
picked up live — the daemon's file watcher rescans on save and re-arms
the timer. To stop firing without removing the source declaration:

```bash
photon ps disable newsletter:sendDigest   # back to DECLARED, persists across restarts
photon ps pause   newsletter:sendDigest   # stays ACTIVE but doesn't fire until resumed
```

### Programmatic schedules with `this.schedule`

Use this when the cron, name, or method is data — for example, a
`reminders` photon where users add their own reminder times.

```typescript
export default class Reminders {
  async addReminder(params: { name: string; cron: string; message: string }) {
    return await this.schedule.create({
      name:     params.name,
      schedule: params.cron,
      method:   'fire',
      params:   { message: params.message },
    });
  }

  async removeReminder(params: { name: string }) {
    return await this.schedule.cancelByName(params.name);
  }

  async list() {
    return await this.schedule.list();
  }

  // The method that actually runs each time the cron fires.
  async fire(params: { message: string }) {
    // ... send the reminder
  }
}
```

`this.schedule.create()` returns a `ScheduledTask` with a stable `id`. The
daemon registers the timer immediately and persists the schedule to
`{base}/.data/{photon}/schedules/{uuid}.json`. Cancellation (by id or
name) unlinks the file and evicts the timer in one step.

> **Pre-1.27.0 gotcha (fixed)**: an `enable_schedule` method that called
> `cancelByName` followed by `create` could silently end up with no
> active timer if the daemon was in a recovery window. Fixed in v1.27.0.
> Regression tests live in `tests/schedule-cancel-create-regression.test.ts`
> and `tests/schedule-ghost-cancel.test.ts`.

### `photon ps` command reference

`photon ps` is the daemon's process viewer. Without arguments it prints
a four-section snapshot — ACTIVE schedules, DECLARED-but-not-enrolled,
WEBHOOKS, and ACTIVE SESSIONS — for every PHOTON_DIR the daemon serves.

```bash
photon ps                          # full snapshot, all bases
photon ps --json                   # same, structured for scripts
photon ps --type active            # one section only
photon ps --base ~/Projects/kith   # filter to one PHOTON_DIR
```

Subcommands:

| Command                              | What it does                                                                   |
|--------------------------------------|--------------------------------------------------------------------------------|
| `photon ps enable <photon>:<method>` | Move a DECLARED `@scheduled` to ACTIVE. Writes to `{base}/.data/.active-schedules.json`. |
| `photon ps disable <photon>:<method>`| Stop firing AND record the disable so a daemon restart doesn't re-enroll. Use this for "I never want this to fire on this machine." |
| `photon ps pause <photon>:<method>`  | Keep enrollment but skip ticks until resumed. Use this for short-lived suppression (e.g. while debugging). |
| `photon ps resume <photon>:<method>` | Undo pause.                                                                    |
| `photon ps history <photon>:<method>`| Show recent firings: timestamp, duration, success/failure. `--limit N`, `--since <iso>`, `--json`. |

Manual cron schedules without a `@scheduled` tag are added through the
Beam UI's Pulse panel ("Add schedule") or programmatically via
`this.schedule.create()` from photon code. They land in the active list
with the cron string embedded so the boot loader can re-arm them after
a daemon restart.

If the same photon name lives in two PHOTON_DIRs and the command would be
ambiguous, every subcommand accepts `--base <dir>` to disambiguate.

### Where state lives

Per-base, under `{base}/.data/`:

```
{base}/.data/.active-schedules.json   # which @scheduled methods are ACTIVE,
                                       # plus the suppressed list (persistent disables)
                                       # and migratedFromAutoRegister flag
{base}/.data/{photon}/schedules/      # ScheduleProvider files (this.schedule.create)
{base}/.data/{photon}/state/          # @stateful instance state
```

Daemon-wide, under `~/.photon/.data/`:

```
~/.photon/.data/.bases.json   # every PHOTON_DIR the daemon has served
~/.photon/.data/daemon.log    # boot, fire, error events — first stop for "why isn't this firing"
~/.photon/.data/daemon.sock   # IPC socket
```

### Multi-host setups: `.photon-no-host`

If you run the same `~/Projects/<base>` on multiple machines (Syncthing,
Dropbox, NFS, etc.) you only want one machine to actually fire the
schedules. Put a marker file in the base on every quiet machine:

```bash
touch ~/Projects/kith/.photon-no-host
```

The daemon refuses to load any ScheduleProvider files, register any
`@scheduled` annotations, or wire any `@webhook` routes for that base.
`photon run` and direct CLI invocations still work — host mode only
suppresses background activation. Remove the file to re-enable.

### Diagnosing "my schedule isn't firing"

1. **`photon ps`** — is your method in ACTIVE? If it's in DECLARED, run
   `photon ps enable <photon>:<method>`.
2. **`photon ps history <photon>:<method>`** — has it ever fired? Most
   recent attempt's error is right there.
3. **`tail -f ~/.photon/.data/daemon.log | grep <photon>`** — boot
   discovery, registration, fire attempts. Filter for the photon name.
4. **`.photon-no-host`** in the base? — host-disabled. Daemon log will
   say `Skipping … host-disabled base`.
5. **Was the daemon restarted recently?** — a few seconds after restart,
   `loadAllPersistedSchedules` (sync) has populated ACTIVE for
   `this.schedule.create()` jobs but `discoverProactiveMetadataAtBoot`
   (async) may still be scanning sources for `@scheduled`. Wait a few
   seconds and re-run `photon ps`.

---

## Common Patterns

### Filesystem Operations

```typescript
import { readFile, writeFile, readdir } from 'fs/promises';
import { join, resolve, relative } from 'path';
import { existsSync } from 'fs';
import { homedir } from 'os';

export default class Filesystem {
  constructor(
    private workdir: string = join(homedir(), 'Documents'),
    private maxFileSize: number = 10485760
  ) {
    if (!existsSync(workdir)) {
      throw new Error(`Directory does not exist: ${workdir}`);
    }
  }

  async read(params: { path: string }) {
    const fullPath = this._resolvePath(params.path);
    const content = await readFile(fullPath, 'utf-8');
    return { success: true, content };
  }

  async write(params: { path: string; content: string }) {
    const fullPath = this._resolvePath(params.path);
    await writeFile(fullPath, params.content, 'utf-8');
    return { success: true, path: fullPath };
  }

  // Security: prevent directory traversal
  private _resolvePath(path: string): string {
    const resolved = resolve(this.workdir, path);
    const rel = relative(this.workdir, resolved);

    if (rel.startsWith('..')) {
      throw new Error('Access denied: path outside working directory');
    }

    return resolved;
  }
}
```

### HTTP Requests

```typescript
export default class Fetch {
  constructor(
    private timeout: number = 5000,
    private maxRedirects: number = 5
  ) {}

  async get(params: { url: string; headers?: Record<string, string> }) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(params.url, {
        method: 'GET',
        headers: params.headers,
        signal: controller.signal,
        redirect: 'follow'
      });

      const data = await response.text();

      return {
        success: true,
        status: response.status,
        data,
        headers: Object.fromEntries(response.headers)
      };
    } catch (error: any) {
      return { success: false, error: error.message };
    } finally {
      clearTimeout(timeoutId);
    }
  }

  async post(params: {
    url: string;
    body: string | object;
    headers?: Record<string, string>;
  }) {
    const body = typeof params.body === 'string'
      ? params.body
      : JSON.stringify(params.body);

    const headers = {
      'Content-Type': 'application/json',
      ...params.headers
    };

    const response = await fetch(params.url, {
      method: 'POST',
      headers,
      body
    });

    return {
      success: response.ok,
      status: response.status,
      data: await response.text()
    };
  }
}
```

### Database Operations

```typescript
import Database from 'better-sqlite3';
import { join } from 'path';
import { homedir } from 'os';

export default class Sqlite {
  private db?: Database.Database;

  constructor(
    private dbPath: string = join(homedir(), 'data.db'),
    private readonly: boolean = false
  ) {}

  async onInitialize() {
    this.db = new Database(this.dbPath, {
      readonly: this.readonly
    });
    console.error(`Database ready: ${this.dbPath}`);
  }

  async onShutdown() {
    this.db?.close();
  }

  async query(params: { sql: string; params?: any[] }) {
    if (!this.db) throw new Error('Database not initialized');

    try {
      const stmt = this.db.prepare(params.sql);
      const rows = params.params
        ? stmt.all(...params.params)
        : stmt.all();

      return { success: true, rows };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  }

  async execute(params: { sql: string; params?: any[] }) {
    if (!this.db) throw new Error('Database not initialized');

    try {
      const stmt = this.db.prepare(params.sql);
      const result = params.params
        ? stmt.run(...params.params)
        : stmt.run();

      return {
        success: true,
        changes: result.changes,
        lastInsertRowid: result.lastInsertRowid
      };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  }
}
```

### Shell Commands

```typescript
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export default class Git {
  constructor(
    private repoPath: string = process.cwd(),
    private timeout: number = 10000
  ) {}

  async status(params: {}) {
    return this._exec('git status --porcelain');
  }

  async log(params: { count?: number }) {
    const count = params.count || 10;
    return this._exec(`git log -n ${count} --oneline`);
  }

  async commit(params: { message: string }) {
    return this._exec(`git commit -m "${params.message}"`);
  }

  private async _exec(command: string) {
    try {
      const { stdout, stderr } = await execAsync(command, {
        cwd: this.repoPath,
        timeout: this.timeout
      });

      return {
        success: true,
        output: stdout || stderr
      };
    } catch (error: any) {
      return {
        success: false,
        error: error.message
      };
    }
  }
}
```

---

## CLI Command Reference

Photon provides a comprehensive suite of commands for running, managing, and developing MCPs.

### Runtime Commands
| Command | Usage |
|---|---|
| `photon mcp <name>` | Run a Photon as an MCP server. Use `--dev` for hot-reload. |
| `photon cli <photon> [method]` | Execute Photon methods directly from the command line. |
| `photon sse <name>` | Launch a single-tenant SSE server for browser or remote access. |
| `photon beam` | Open an interactive web UI for all your installed Photons (formerly playground). |
| `photon beam owner/repo/name` | Install a photon from GitHub and open it in Beam. |
| `photon cli owner/repo/name method` | Install a photon from GitHub and run a method. |
| `photon serve` | Start a local multi-tenant MCP server host for development. |
| `photon host <command>` | Manage cloud hosting: `deploy` (ship to cloud) or `preview` (run local sim). |

### Management Commands
| Command | Usage |
|---|---|
| `photon add <name>` | Install a Photon from the marketplace. |
| `photon add owner/repo/name` | Install a Photon directly from a GitHub repository. |
| `photon remove <name>` | Remove an installed Photon. |
| `photon upgrade [name]` | Upgrade Photon(s) to the latest version. |
| `photon info [name]` | Show detailed metadata and configuration for a Photon. |
| `photon search <query>` | Search enabled marketplaces for Photons. |

### Build Commands
| Command | Usage |
|---|---|
| `photon build <name>` | Compile a photon into a standalone binary (uses Bun). |
| `photon build <name> --with-app` | Include embedded Beam UI in the binary. |
| `photon build <name> -t <target>` | Cross-compile for a specific platform (e.g., `bun-linux-x64`). |

### Developer Tools (maker)
| Command | Usage |
|---|---|
| `photon maker new <name>` | Create a new Photon from the default template. |
| `photon maker validate <name>` | Validate syntax, schemas, and dependencies. |
| `photon maker sync` | Generate `photons.json` manifest for a marketplace. |
| `photon maker init` | Set up a marketplace with auto-sync git hooks. |
| `photon maker diagram <name>` | Generate a Mermaid dependency/flow diagram. |

### Maintenance
| Command | Usage |
|---|---|
| `photon doctor` | Diagnose your environment (Node, npm, ports, config). |
| `photon update` | Refresh marketplace indexes and check for CLI updates. |
| `photon clear-cache` | Clear compiled Photon artifacts. |

### Advanced
| Command | Usage |
|---|---|
| `photon test [name]` | Run test methods defined in your Photons (supports unit/integration). |
| `photon alias <name>` | Create a global CLI alias for a Photon (e.g. `run-my-tool`). |
| `photon marketplace` | Manage marketplace sources (add/remove git repos). |

---

## Testing and Development

### Local Development

**1. Create MCP:**
```bash
photon maker new my-tool
```

**2. Edit file:**
```bash
# Opens ~/.photon/my-tool.photon.ts
code ~/.photon/my-tool.photon.ts
```

**3. Run in dev mode:**
```bash
photon mcp my-tool --dev
```

Dev mode features:
- ✅ Hot reload on file changes
- ✅ Detailed error messages
- ✅ Console logging visible

**4. Validate:**
```bash
photon maker validate my-tool
```

Shows:
- Tool count
- Schema extraction results
- Compilation errors

### Testing with MCP Inspector

Use the official MCP Inspector:

```bash
# Install globally
bun add -g @modelcontextprotocol/inspector

# Test your MCP
bunx @modelcontextprotocol/inspector photon my-tool.photon.ts

# pnpm alternative
pnpm dlx @modelcontextprotocol/inspector photon my-tool.photon.ts
```

### Manual Testing

Create a test script:

```typescript
// test.ts
import { join } from 'path';
import { homedir } from 'os';

async function test() {
  // Import your MCP class
  const { default: MyMCP } = await import('./my-tool.photon.ts');

  // Instantiate with test config
  const mcp = new MyMCP(join(homedir(), 'test-data'));

  // Initialize
  await mcp.onInitialize?.();

  // Test tools
  const result = await mcp.myTool({ input: 'test' });
  console.log('Result:', result);

  // Cleanup
  await mcp.onShutdown?.();
}

test().catch(console.error);
```

Run:
```bash
bunx tsx test.ts
```

### Debugging

**Enable verbose logging:**
```typescript
async onInitialize() {
  console.error('Configuration:');
  console.error(JSON.stringify({
    workdir: this.workdir,
    enabled: this.enabled
  }, null, 2));
}
```

**Check environment variables:**
```bash
# List all environment variables
env | grep MY_TOOL

# Run with specific vars
MY_TOOL_WORKDIR=/tmp/test photon my-tool --dev
```

**Validate schemas:**
```bash
photon maker validate my-tool
```

---

## Deployment

### Claude Desktop

**1. Generate config:**
```bash
photon info my-tool --mcp
```

**Output:**
```json
{
  "mcpServers": {
    "my-tool": {
      "command": "bunx",
      "args": ["-y", "@portel/photon", "mcp", "my-tool"],
      "env": {
        "MY_TOOL_WORKDIR": "~/Documents",
        "MY_TOOL_MAX_FILE_SIZE": "10485760"
      }
    }
  }
}
```

**2. Add to Claude Desktop config:**

macOS:
```bash
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
```

Windows:
```bash
code %APPDATA%\Claude\claude_desktop_config.json
```

**3. Restart Claude Desktop**

### Claude Code CLI

Add to `.claude/claude.json`:

```json
{
  "mcpServers": {
    "my-tool": {
      "command": "photon",
      "args": ["mcp", "my-tool"],
      "env": {
        "MY_TOOL_WORKDIR": "${workspaceFolder}/data"
      }
    }
  }
}
```

### Cursor/Windsurf

Add to MCP settings:

```json
{
  "mcpServers": {
    "my-tool": {
      "command": "bunx",
      "args": ["-y", "@portel/photon", "mcp", "my-tool"]
    }
  }
}
```

### Environment Variables

**Option 1: In MCP config (recommended):**
```json
{
  "my-tool": {
    "command": "photon",
    "args": ["mcp", "my-tool"],
    "env": {
      "MY_TOOL_API_KEY": "sk-...",
      "MY_TOOL_ENDPOINT": "https://api.example.com"
    }
  }
}
```

**Option 2: System environment:**
```bash
export MY_TOOL_API_KEY="sk-..."
photon mcp my-tool
```

**Option 3: .env file (not recommended for production):**
```bash
# .env
MY_TOOL_API_KEY=sk-...
```

### Cloudflare Workers

Test your Photon locally in a simulated Cloudflare environment:
```bash
photon host preview cf my-tool
```

Deploy your Photon to the edge with Cloudflare Workers:
```bash
photon host deploy cloudflare my-tool
```

This will:
1. Generate an optimized bundle.
2. Create a `wrangler.toml` configuration with a Durable Object binding for the photon.
3. Deploy the service to your Cloudflare account.

Use `--dev` to enable the interactive playground in the deployed worker, and
`--logs` to opt into Cloudflare Workers Logs for dashboard observability.

**Stateful photons.** `this.memory`, `this.emit`, `this.schedule`,
`this.call`, `this.sample`, `this.confirm`, and `this.elicit` all work
the same on Cloudflare as on the local daemon. Each `instanceName` runs
in its own Durable Object — storage is backed by `ctx.storage`,
`this.emit({channel, ...})` broadcasts to subscribers connected at
`wss://<worker>/events?channel=<name>`, `this.schedule.create({...})`
registers cron entries that fire from the DO's alarm, `this.call('foo.bar', ...)`
hops to a sibling photon DO declared in the host's `@photons`, and
`this.sample` / `this.confirm` / `this.elicit` ride MCP server-initiated
requests on the active tool call's SSE response (clients must signal
`Accept: text/event-stream`). Pick the instance per request with
`?instance=<name>` or the `X-Photon-Instance` header; omit both to land
on the shared `'default'` singleton. See
[`docs/internals/CF-DURABLE-OBJECTS.md`](internals/CF-DURABLE-OBJECTS.md)
for the full capability mapping.

---

## How Photon Works

Understanding Photon's internals helps debug issues and optimize performance.

### Architecture Overview

```
┌─────────────────────────────────────────┐
│         .photon.ts file                 │
│  export default class MyMCP { ... }     │
└────────────────┬────────────────────────┘
                 │
                 ↓
┌─────────────────────────────────────────┐
│           Loader (loader.ts)            │
│  1. Compile TypeScript → JavaScript     │
│  2. Extract constructor parameters      │
│  3. Resolve environment variables       │
│  4. Instantiate class with config       │
└────────────────┬────────────────────────┘
                 │
                 ↓
┌─────────────────────────────────────────┐
│      Schema Extractor (schema.ts)       │
│  1. Parse JSDoc comments                │
│  2. Extract TypeScript types            │
│  3. Generate JSON schemas               │
└────────────────┬────────────────────────┘
                 │
                 ↓
┌─────────────────────────────────────────┐
│         MCP Server (server.ts)          │
│  1. Implement MCP protocol              │
│  2. List tools (from public methods)    │
│  3. Call tools (invoke class methods)   │
│  4. Handle lifecycle (init/shutdown)    │
└────────────────┬────────────────────────┘
                 │
                 ↓
┌─────────────────────────────────────────┐
│      stdio/JSON-RPC Transport           │
│  Communicate with MCP clients via       │
│  standard input/output                  │
└─────────────────────────────────────────┘
```

### Photon ID

Every photon instance is assigned a unique 12-character hash ID based on its file path:

```typescript
// Generated from path using SHA-256
// Path: /Users/you/.photon/kanban.photon.ts
// ID:   f5c5ee47905e

import { createHash } from 'crypto';

function generatePhotonId(photonPath: string): string {
  return createHash('sha256').update(photonPath).digest('hex').slice(0, 12);
}
```

**Why hashed IDs?**
- **Unique across systems**: Different installations get different IDs
- **Stable**: Same path always produces same ID
- **Multi-tenant safe**: No collisions between different users or projects
- **Short**: 12 characters is enough for practical uniqueness

**Where IDs are used:**
- **Channel subscriptions**: `{photonId}:{itemId}` format for daemon pub/sub
- **MCP responses**: `x-photon-id` header in tools/list
- **PhotonInfo**: `id` field in photon metadata

**Access in code:**

```typescript
// In your photon (via PhotonMCP base class)
export default class MyPhoton extends PhotonMCP {
  async myMethod() {
    console.log('My ID:', this.photonId);  // e.g., "f5c5ee47905e"
  }
}

// In Custom UI (via bridge)
const photonId = photon.photonId;
```

### Compilation Process

**1. Source → JavaScript:**
```typescript
// Input: calculator.photon.ts
export default class Calculator {
  async add(params: { a: number; b: number }) {
    return params.a + params.b;
  }
}

// Output: Compiled JavaScript (ESM)
export default class Calculator {
  async add(params) {
    return params.a + params.b;
  }
}
```

**Tool:** esbuild (fast TypeScript compiler)

**Cache:** `~/.photon/.data/.cache/compiled/{hash}.js`

**2. Constructor Parameter Extraction:**
```typescript
// From source file (not compiled)
constructor(private workdir: string = '/default')

// Extracted parameters:
[
  {
    name: 'workdir',
    type: 'string',
    hasDefault: true,
    defaultValue: '/default'
  }
]
```

**3. Environment Variable Resolution:**
```typescript
// Parameter: workdir
// MCP name: filesystem
// Env var: FILESYSTEM_WORKDIR

const envValue = process.env.FILESYSTEM_WORKDIR;
const finalValue = envValue || defaultValue;
```

### Schema Extraction

**Input (TypeScript + JSDoc):**
```typescript
/**
 * Add two numbers together
 * @param a First number
 * @param b Second number
 */
async add(params: { a: number; b: number }) {
  return params.a + params.b;
}
```

**Output (JSON Schema):**
```json
{
  "name": "add",
  "description": "Add two numbers together",
  "inputSchema": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number",
        "description": "First number"
      },
      "b": {
        "type": "number",
        "description": "Second number"
      }
    },
    "required": ["a", "b"]
  }
}
```

**Process:**
1. Parse JSDoc with regex
2. Extract TypeScript types from source
3. Map TS types → JSON Schema types
4. Combine descriptions with schemas

### MCP Protocol Implementation

**Tool listing:**
```json
{
  "jsonrpc": "2.0",
  "method": "tools/list",
  "result": {
    "tools": [
      {
        "name": "add",
        "description": "Add two numbers together",
        "inputSchema": { ... }
      }
    ]
  }
}
```

**Tool call:**
```json
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "add",
    "arguments": { "a": 5, "b": 3 }
  }
}
```

**Response:**
```json
{
  "jsonrpc": "2.0",
  "result": {
    "content": [
      { "type": "text", "text": "8" }
    ]
  }
}
```

### Hot Reload

In `--dev` mode:

1. **Watch file:** `chokidar` monitors `.photon.ts`
2. **On change:**
   - Recompile with esbuild
   - Reload class dynamically
   - Re-extract schemas
   - Update tool registry
3. **Server continues:** No restart needed

### Cache and Resilience

Photon caches compiled artifacts and installed dependencies so repeated startups are fast. Here is where things live and what happens when they go stale.

**Dependency cache**: `~/.photon/.data/.cache/dependencies/{cacheKey}/`

Each photon's `@dependencies` get their own isolated `node_modules`. The cache key is derived from the dependency list, so changing your `@dependencies` tag naturally creates a fresh cache.

**Auto-invalidation on photon-core updates**: The cache tracks the *resolved* version of `@portel/photon-core` (not the semver range). If you upgrade photon-core from 2.5.3 to 2.5.4, the next run detects the mismatch and rebuilds the dependency cache automatically. No manual intervention, no mysterious "it works on my machine" moments.

**Daemon auto-restart**: If the daemon process has crashed or become unreachable (ECONNREFUSED), the next CLI command automatically restarts it and retries the operation. You might notice a slightly longer first response, but that is the only visible sign.

**Manual cache clearing**: When all else fails:
```bash
photon clear-cache
```

This wipes compiled artifacts and dependency caches. The next run rebuilds everything from scratch.

---

## Best Practices

### Security

**1. Path Traversal Protection:**
```typescript
private _resolvePath(userPath: string): string {
  const resolved = resolve(this.workdir, userPath);
  const rel = relative(this.workdir, resolved);

  if (rel.startsWith('..') || resolve(rel) === rel) {
    throw new Error('Access denied: path outside working directory');
  }

  return resolved;
}
```

**2. Input Validation:**
```typescript
async process(params: { email: string }) {
  // Validate format
  if (!params.email.includes('@')) {
    return { success: false, error: 'Invalid email format' };
  }

  // Sanitize input
  const email = params.email.trim().toLowerCase();

  // Process safely
  return await this._processEmail(email);
}
```

**3. Command Injection Prevention:**
```typescript
// ❌ BAD: Direct string interpolation
async git(params: { message: string }) {
  await exec(`git commit -m "${params.message}"`); // Vulnerable!
}

// ✅ GOOD: Use parameterized commands
async git(params: { message: string }) {
  // Escape or use library
  const escaped = params.message.replace(/"/g, '\\"');
  await exec(`git commit -m "${escaped}"`);
}

// ✅ BETTER: Use child_process with args array
import { spawn } from 'child_process';
async git(params: { message: string }) {
  return new Promise((resolve) => {
    spawn('git', ['commit', '-m', params.message]);
  });
}
```

**4. File Size Limits:**
```typescript
async read(params: { path: string }) {
  const fullPath = this._resolvePath(params.path);
  const stats = await stat(fullPath);

  if (stats.size > this.maxFileSize) {
    return {
      success: false,
      error: `File too large: ${stats.size} bytes (max: ${this.maxFileSize})`
    };
  }

  return { success: true, content: await readFile(fullPath, 'utf-8') };
}
```

### Performance

**1. Lazy Initialization:**
```typescript
export default class Database {
  private db?: DatabaseConnection;

  async query(params: { sql: string }) {
    if (!this.db) {
      this.db = await this._connect();
    }
    return this.db.execute(params.sql);
  }
}
```

**2. Connection Pooling:**
```typescript
export default class HTTP {
  private agent?: Agent;

  constructor() {
    this.agent = new Agent({
      keepAlive: true,
      maxSockets: 10
    });
  }

  async fetch(params: { url: string }) {
    return fetch(params.url, { agent: this.agent });
  }
}
```

**3. Streaming Large Files:**
```typescript
async readLarge(params: { path: string }) {
  const stream = createReadStream(this._resolvePath(params.path));
  let content = '';

  for await (const chunk of stream) {
    content += chunk;
  }

  return { success: true, content };
}
```

### Error Handling

**1. Structured Errors:**
```typescript
async process(params: { input: string }) {
  try {
    // Validate
    if (!params.input) {
      return {
        success: false,
        error: 'Input is required',
        code: 'MISSING_INPUT'
      };
    }

    // Process
    const result = await this._process(params.input);

    return {
      success: true,
      result,
      timestamp: new Date().toISOString()
    };
  } catch (error: any) {
    return {
      success: false,
      error: error.message,
      code: error.code || 'UNKNOWN_ERROR'
    };
  }
}
```

**2. Graceful Degradation:**
```typescript
async fetch(params: { url: string }) {
  try {
    return await this._fetchWithRetry(params.url, 3);
  } catch (error) {
    // Log but don't crash
    console.error('Fetch failed:', error);
    return {
      success: false,
      error: 'Service temporarily unavailable'
    };
  }
}
```

### Documentation

**1. Comprehensive JSDoc:**
```typescript
/**
 * Search for text patterns in files (grep-like functionality)
 *
 * Recursively searches through files in the specified directory,
 * matching lines that contain the search pattern. Results include
 * file paths, line numbers, and matched content.
 *
 * @param pattern Text pattern to search for (case-sensitive)
 * @param path Directory to search in (relative to working directory, default: root)
 * @param filePattern Optional file pattern (e.g., "*.ts" for TypeScript files)
 * @returns List of matches with file, line number, and content
 */
async search(params: {
  pattern: string;
  path?: string;
  filePattern?: string;
}) {
  // Implementation
}
```

**2. File Header:**
```typescript
/**
 * Filesystem - File and directory operations
 *
 * Provides essential file system utilities: read, write, list, search, delete.
 * All paths are resolved relative to the configured working directory for security.
 *
 * Common use cases:
 * - Organize documents: "Categorize my documents by topic"
 * - Search files: "Find all PDFs about project planning"
 * - Bulk operations: "Move all .txt files to Archive folder"
 *
 * Configuration:
 * - workdir: Working directory (default: ~/Documents)
 * - maxFileSize: Max file size in bytes (default: 10MB)
 * - allowHidden: Allow hidden files (default: false)
 *
 * Dependencies: None (uses Node.js built-in fs)
 *
 * @version 2.0.0
 * @author Your Name
 * @license MIT
 */
```

---

## Troubleshooting

### Common Issues

**1. "Cannot find module" error:**

```
Error: Cannot find module 'my-dependency'
```

**Solution:** Install dependencies in the same directory:
```bash
cd ~/.photon
bun add my-dependency
```

**2. Environment variables not working:**

```
Error: API key is required
```

**Solution:** Check environment variable naming:
```bash
# For MCP named "my-tool" with parameter "apiKey"
export MY_TOOL_API_KEY="your-key"

# Or use --config to see correct names
photon my-tool --config
```

**3. Constructor validation fails:**

```
Error: Working directory does not exist: /invalid/path
```

**Solution:** The MCP loads but tools fail with helpful error. Users see:
```
Configuration Error: Working directory does not exist: /invalid/path

To configure this MCP, set environment variables:
  FILESYSTEM_WORKDIR=~/Documents

Or add to your MCP config:
{
  "env": {
    "FILESYSTEM_WORKDIR": "/path/to/docs"
  }
}
```

**4. Hot reload not working:**

**Solution:** Check file permissions and paths:
```bash
# Ensure file is writable
chmod +w ~/.photon/my-tool.photon.ts

# Check for syntax errors
photon maker validate my-tool
```

**5. Schema extraction fails:**

```
Warning: Could not extract schema for tool 'myTool'
```

**Solution:** Ensure proper TypeScript types:
```typescript
// ❌ BAD: No type annotations
async myTool(params) { }

// ✅ GOOD: Explicit types
async myTool(params: { input: string }) { }
```

### Debugging Tips

**1. Enable verbose logging:**
```typescript
async onInitialize() {
  console.error('[debug] Configuration:', this);
  console.error('[debug] Environment:', process.env);
}
```

**2. Validate schemas:**
```bash
photon maker validate my-tool
```

Shows:
- ✅ Tools found: 5
- ✅ Schemas extracted: 5
- ❌ Compilation errors

**3. Test compilation:**
```bash
# Compile manually
bunx esbuild my-tool.photon.ts --bundle --platform=node --format=esm
```

**4. Check MCP protocol:**
```bash
# Use MCP Inspector
bunx @modelcontextprotocol/inspector photon my-tool
```

**5. Verify environment:**
```bash
# List environment variables
env | grep MY_TOOL

# Test with specific values
MY_TOOL_DEBUG=true photon my-tool --dev
```

### Getting Help

1. **Check examples:** [photon-examples](https://github.com/portel-dev/photon-examples) has working MCPs
2. **Read logs:** stderr output shows detailed error messages
3. **Validate:** Use `photon maker validate my-tool`
4. **GitHub Issues:** https://github.com/portel-dev/photon/issues
5. **MCP Docs:** https://modelcontextprotocol.io/

---

## Advanced Topics

### Custom Type Mappings

For complex types, use JSDoc to guide schema generation:

```typescript
/**
 * Process user data
 * @param user User object with name, age, and email
 */
async process(params: {
  user: {
    name: string;
    age: number;
    email: string;
  }
}) {
  // Photon extracts nested object schema automatically
}
```

### Pre-generated Schemas

For bundled MCPs, create `.photon.schema.json`:

```json
[
  {
    "name": "add",
    "description": "Add two numbers",
    "inputSchema": {
      "type": "object",
      "properties": {
        "a": { "type": "number" },
        "b": { "type": "number" }
      },
      "required": ["a", "b"]
    }
  }
]
```

Photon will use this instead of extracting from source.

### Multi-File MCPs

While Photon is designed for single-file MCPs, you can import utilities:

```typescript
// helpers.ts
export function sanitize(input: string) {
  return input.trim().toLowerCase();
}

// my-tool.photon.ts
import { sanitize } from './helpers.js';

export default class MyTool {
  async process(params: { input: string }) {
    return sanitize(params.input);
  }
}
```

Compile with esbuild's bundling (automatic in Photon).

---

## Summary

**Key Takeaways:**

1. **Single File** - One `.photon.ts` = one MCP server
2. **No Config** - Convention over configuration
3. **Constructor → Env Vars** - Automatic config injection
4. **Public Methods → Tools** - No decorators needed
5. **JSDoc → Descriptions** - Documentation becomes MCP metadata
6. **TypeScript → JSON Schema** - Type safety built-in
7. **Lifecycle Hooks** - Optional `onInitialize` and `onShutdown`
8. **Hot Reload** - Dev mode for rapid iteration

**Next Steps:**

1. Create your first MCP: `photon maker new my-tool`
2. Study examples: [photon-examples](https://github.com/portel-dev/photon-examples)
3. Test in dev mode: `photon mcp my-tool --dev`
4. Deploy to Claude Desktop: `photon info my-tool --mcp`

Happy building! 🚀
