# Advanced Photon Guide

Deep dive into Photon's advanced features, patterns, and best practices.

## Table of Contents

- [Lifecycle Hooks](#lifecycle-hooks)
- [Advanced Type Patterns](#advanced-type-patterns)
- [Manual Schema Overrides](#manual-schema-overrides)
- [Performance Optimization](#performance-optimization)
- [Error Handling Strategies](#error-handling-strategies)
- [Testing MCPs](#testing-mcps)
- [Production Deployment](#production-deployment)
- [Custom Marketplace Setup](#custom-marketplace-setup)
- [Integration Patterns](#integration-patterns)

---

## Lifecycle Hooks

### onInitialize()

Called once when MCP starts. Use for setup tasks.

```typescript
export default class Database {
  private connection?: DatabaseConnection;

  constructor(
    private connectionString: string,
    private poolSize: number = 10
  ) {}

  async onInitialize() {
    console.error('Establishing database connection...');

    this.connection = await createConnection({
      url: this.connectionString,
      pool: {
        min: 2,
        max: this.poolSize,
      },
    });

    // Test connection
    await this.connection.query('SELECT 1');
    console.error('Database connected successfully');
  }

  async query(params: { sql: string }) {
    if (!this.connection) {
      throw new Error('Database not initialized');
    }
    return await this.connection.query(params.sql);
  }
}
```

**Best Practices**:
- ✅ DO: Establish connections, load configuration
- ✅ DO: Throw errors if initialization fails
- ✅ DO: Add logging for debugging
- ❌ DON'T: Do expensive operations that aren't required
- ❌ DON'T: Load large datasets into memory

### onShutdown()

Called when MCP is stopping. Clean up resources.

```typescript
export default class WebSocket {
  private connections: Connection[] = [];
  private timers: NodeJS.Timer[] = [];

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

    // Clear timers
    this.timers.forEach(timer => clearInterval(timer));

    // Close connections
    await Promise.all(
      this.connections.map(conn =>
        conn.close().catch(err =>
          console.error('Error closing connection:', err)
        )
      )
    );

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

**Critical for**:
- Database connections
- File handles
- WebSocket connections
- Timers/intervals
- Child processes

---

## Advanced Type Patterns

### Nested Objects

```typescript
export default class UserManagement {
  /**
   * Create a new user with profile
   * @param user User details including profile information
   */
  async createUser(params: {
    user: {
      name: string;
      email: string;
      profile: {
        bio?: string;
        avatar?: string;
        social: {
          twitter?: string;
          github?: string;
        };
      };
    };
  }) {
    // Full type safety with nested validation
    return {
      id: generateId(),
      ...params.user,
    };
  }
}
```

### Union Types

```typescript
/**
 * Process data with multiple input formats
 * @param input Either JSON string or object
 */
async processData(params: {
  input: string | { data: any };
  format: 'json' | 'yaml' | 'xml';
}) {
  // Photon generates: anyOf: [{ type: 'string' }, { type: 'object' }]
  const data = typeof params.input === 'string'
    ? JSON.parse(params.input)
    : params.input;

  return convertFormat(data, params.format);
}
```

### Arrays with Constraints

```typescript
/**
 * Batch process items
 * @param items Array of items to process (max 100)
 */
async batchProcess(params: {
  items: Array<{
    id: string;
    data: any;
  }>;
}) {
  if (params.items.length > 100) {
    throw new Error('Maximum 100 items per batch');
  }

  return await Promise.all(
    params.items.map(item => this.processItem(item))
  );
}
```

### Enums with Literal Types

```typescript
/**
 * Set log level
 * @param level Log level (debug, info, warn, error)
 */
async setLogLevel(params: {
  level: 'debug' | 'info' | 'warn' | 'error';
}) {
  // Photon generates enum constraint
  logger.setLevel(params.level);
  return { level: params.level };
}
```

---

## Manual Schema Overrides

When TypeScript's auto-extraction doesn't cover edge cases (complex imported types, type aliases, or dynamic schemas), you can manually specify schemas using a `.schema.json` file.

### When to Use

Use manual overrides when:
- Using complex imported types that can't be inlined
- Type aliases that reference external definitions
- Dynamic schemas that vary at runtime
- Third-party types from libraries
- Schemas with advanced JSON Schema features

### Format

Create a `.schema.json` file next to your `.photon.ts` MCP file:

```
my-mcp.photon.ts
my-mcp.schema.json  ← Manual schema override
```

### Schema Structure

```json
{
  "tools": [
    {
      "name": "toolName",
      "description": "Tool description",
      "inputSchema": {
        "type": "object",
        "properties": {
          "param": { "type": "string" }
        },
        "required": ["param"]
      }
    }
  ],
  "templates": [
    {
      "name": "templateName",
      "description": "Template description",
      "inputSchema": { /* ... */ }
    }
  ],
  "statics": [
    {
      "name": "staticName",
      "uri": "static://path",
      "description": "Static resource description",
      "mimeType": "application/json",
      "inputSchema": { /* ... */ }
    }
  ]
}
```

### Example: Complex Type Alias

```typescript
// my-mcp.photon.ts
import { ComplexFilter } from './types'; // Can't be auto-extracted

export default class SearchMCP {
  /**
   * Search with complex filters
   */
  async search(params: {
    query: string;
    filters: ComplexFilter; // Type alias - won't auto-extract properly
  }) {
    // Implementation
  }
}
```

```json
// my-mcp.schema.json
{
  "tools": [
    {
      "name": "search",
      "description": "Search with complex filters",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Search query"
          },
          "filters": {
            "type": "object",
            "description": "Advanced filter options",
            "properties": {
              "tags": {
                "type": "array",
                "items": { "type": "string" }
              },
              "dateRange": {
                "type": "object",
                "properties": {
                  "from": { "type": "string", "format": "date" },
                  "to": { "type": "string", "format": "date" }
                }
              },
              "score": {
                "type": "number",
                "minimum": 0,
                "maximum": 100
              }
            }
          }
        },
        "required": ["query"]
      }
    }
  ]
}
```

### Partial Override

You can override specific tools while auto-extracting others:

```json
{
  "tools": [
    {
      "name": "complexTool",
      "inputSchema": { /* manual schema */ }
    }
    // Other tools will be auto-extracted
  ]
}
```

### Validation

Photon validates that:
- Tool/template/static names exist in your TypeScript class
- Schema follows JSON Schema Draft 2020-12
- Required fields are present

### Limitations

- Must manually keep schema in sync with code
- No TypeScript type checking for schema
- Overridden tools skip auto-extraction entirely

### Best Practices

1. **Document why**: Add a comment explaining why manual override is needed
   ```json
   {
     "tools": [{
       "name": "complexQuery",
       "description": "Uses imported GraphQL types that can't be auto-extracted",
       "inputSchema": { /* ... */ }
     }]
   }
   ```

2. **Validate regularly**: Test that manual schemas match actual implementation
   ```bash
   photon mcp my-mcp --validate
   ```

3. **Keep it minimal**: Only override what's necessary, let auto-extraction handle the rest

4. **Version control**: Commit both `.photon.ts` and `.schema.json` together

5. **Use TypeScript for simple cases**: Inline types when possible instead of manual overrides

---

## Performance Optimization

### Lazy Loading

```typescript
export default class AIService {
  private model?: LargeModel;

  // ❌ Slow - loads 2GB model at startup
  async onInitialize() {
    this.model = await loadLargeModel();
  }

  // ✅ Fast - loads on first use
  private async getModel() {
    if (!this.model) {
      console.error('Loading model...');
      this.model = await loadLargeModel();
      console.error('Model loaded');
    }
    return this.model;
  }

  async predict(params: { input: string }) {
    const model = await this.getModel();
    return await model.predict(params.input);
  }
}
```

### Connection Pooling

```typescript
export default class DatabaseOptimized {
  private pool: Pool;

  constructor(private dbUrl: string) {
    // Create pool immediately (cheap)
    this.pool = new Pool({
      connectionString: this.dbUrl,
      max: 20,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000,
    });
  }

  async query(params: { sql: string }) {
    // Reuse connections from pool
    const client = await this.pool.connect();
    try {
      const result = await client.query(params.sql);
      return { rows: result.rows };
    } finally {
      client.release(); // Return to pool
    }
  }

  async onShutdown() {
    await this.pool.end();
  }
}
```

### Caching Strategies

```typescript
export default class API {
  private cache = new Map<string, { data: any; expires: number }>();
  private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes

  /**
   * Fetch data with caching
   * @param endpoint API endpoint to fetch
   */
  async fetch(params: { endpoint: string }) {
    const cached = this.cache.get(params.endpoint);

    if (cached && cached.expires > Date.now()) {
      console.error('Cache hit:', params.endpoint);
      return cached.data;
    }

    console.error('Cache miss, fetching:', params.endpoint);
    const data = await this.fetchFromAPI(params.endpoint);

    this.cache.set(params.endpoint, {
      data,
      expires: Date.now() + this.CACHE_TTL,
    });

    // Limit cache size
    if (this.cache.size > 100) {
      const oldest = this.cache.keys().next().value;
      this.cache.delete(oldest);
    }

    return data;
  }
}
```

### Batch Operations

```typescript
export default class BatchProcessor {
  private queue: Array<{ id: string; data: any }> = [];
  private batchTimer?: NodeJS.Timeout;

  /**
   * Add item to processing queue
   * @param item Item to process
   */
  async addItem(params: { id: string; data: any }) {
    this.queue.push(params);

    // Clear existing timer
    if (this.batchTimer) {
      clearTimeout(this.batchTimer);
    }

    // Process after 100ms or when 50 items queued
    if (this.queue.length >= 50) {
      await this.processBatch();
    } else {
      this.batchTimer = setTimeout(() => this.processBatch(), 100);
    }

    return { queued: true, position: this.queue.length };
  }

  private async processBatch() {
    if (this.queue.length === 0) return;

    const batch = this.queue.splice(0, 50);
    console.error(`Processing batch of ${batch.length} items`);

    await this.processItems(batch);
  }
}
```

---

## Error Handling Strategies

### Graceful Degradation

```typescript
export default class MultiSourceData {
  constructor(
    private primaryAPI: string,
    private fallbackAPI: string
  ) {}

  /**
   * Fetch data with automatic fallback
   * @param id Data ID
   */
  async getData(params: { id: string }) {
    try {
      return await this.fetchFromPrimary(params.id);
    } catch (primaryError) {
      console.error('Primary API failed, trying fallback:', primaryError);

      try {
        const data = await this.fetchFromFallback(params.id);
        return {
          ...data,
          source: 'fallback',
          warning: 'Using fallback data source',
        };
      } catch (fallbackError) {
        throw new Error(
          `Both primary and fallback failed. ` +
          `Primary: ${primaryError.message}, ` +
          `Fallback: ${fallbackError.message}`
        );
      }
    }
  }
}
```

### Retry with Exponential Backoff

```typescript
export default class ResilientAPI {
  /**
   * Fetch with retry logic
   * @param url URL to fetch
   */
  async fetchWithRetry(params: { url: string }) {
    const maxRetries = 3;
    let lastError: Error;

    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        return await fetch(params.url).then(r => r.json());
      } catch (error: any) {
        lastError = error;

        if (attempt < maxRetries - 1) {
          const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
          console.error(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }

    throw new Error(`Failed after ${maxRetries} attempts: ${lastError!.message}`);
  }
}
```

### Input Validation

```typescript
export default class ValidatedAPI {
  /**
   * Create user with validation
   * @param email User email address
   * @param age User age (18-120)
   */
  async createUser(params: { email: string; age: number }) {
    // Validate email
    if (!params.email.includes('@')) {
      throw new Error('Invalid email format');
    }

    // Validate age
    if (params.age < 18 || params.age > 120) {
      throw new Error('Age must be between 18 and 120');
    }

    return await this.saveUser(params);
  }
}
```

---

## Testing MCPs

### Unit Testing

```typescript
// my-mcp.test.ts
import { describe, it, expect } from 'vitest';

// Import your MCP class
import MCP from './my-mcp.photon.js';

describe('MyMCP', () => {
  it('should calculate correctly', async () => {
    const mcp = new MCP();
    const result = await mcp.calculate({ expression: '2 + 2' });
    expect(result.result).toBe(4);
  });

  it('should handle errors', async () => {
    const mcp = new MCP();
    await expect(
      mcp.calculate({ expression: 'invalid' })
    ).rejects.toThrow();
  });
});
```

### Integration Testing

Photon uses **vitest** for unit tests and **tsx** for integration tests. Jest is not used.

```typescript
// Integration test — run with: bunx vitest run
import { describe, it, expect } from 'vitest';
import { execSync } from 'child_process';

describe('MCP Integration', () => {
  it('should list tools via CLI', () => {
    const output = execSync('photon cli my-tool --help', { encoding: 'utf-8' });
    expect(output).toContain('Available methods');
  });

  it('should execute a method', () => {
    const output = execSync('photon cli my-tool greet --name Ada', { encoding: 'utf-8' });
    expect(output).toContain('Hello, Ada');
  });
});
```

> **Note:** There is no `PhotonServer` class exported for user consumption. The MCP server is managed internally by the runtime via `photon mcp <name>`.

---

## Production Deployment

### Docker Deployment

```dockerfile
# Dockerfile
FROM oven/bun:1

WORKDIR /app

# Install Photon globally
RUN bun add -g @portel/photon

# Copy MCP file
COPY my-mcp.photon.ts .

# Set environment variables
ENV MY_MCP_API_KEY=""
ENV NODE_ENV=production

# Start MCP
CMD ["photon", "mcp", "my-mcp"]
```

### Process Manager (PM2)

```json
// ecosystem.config.json
{
  "apps": [
    {
      "name": "github-mcp",
      "script": "photon",
      "args": ["mcp", "github-issues"],
      "env": {
        "GITHUB_ISSUES_TOKEN": "your-token",
        "NODE_ENV": "production"
      },
      "instances": 1,
      "autorestart": true,
      "watch": false,
      "max_memory_restart": "500M"
    }
  ]
}
```

```bash
# Deploy with PM2
pm2 start ecosystem.config.json
pm2 save
pm2 startup
```

### Health Checks

```typescript
export default class HealthMonitored {
  private healthy = true;
  private lastCheck = Date.now();

  async onInitialize() {
    // Start health check interval
    setInterval(() => this.checkHealth(), 30000);
  }

  private async checkHealth() {
    try {
      // Check dependencies
      await this.testConnection();
      this.healthy = true;
      this.lastCheck = Date.now();
    } catch (error) {
      console.error('Health check failed:', error);
      this.healthy = false;
    }
  }

  /**
   * Get health status
   */
  async health() {
    return {
      healthy: this.healthy,
      lastCheck: this.lastCheck,
      uptime: process.uptime(),
    };
  }
}
```

---

## Custom Marketplace Setup

### Creating a Marketplace

```bash
# 1. Create repository
mkdir my-company-mcps
cd my-company-mcps
git init

# 2. Create marketplace manifest
mkdir .marketplace
cat > .marketplace/photons.json << 'EOF'
{
  "name": "my-company-mcps",
  "version": "1.0.0",
  "description": "Internal MCPs for My Company",
  "owner": {
    "name": "My Company",
    "url": "https://example.com"
  },
  "photons": []
}
EOF

# 3. Add MCPs
cat > analytics.photon.ts << 'EOF'
/**
 * Analytics MCP
 * @version 1.0.0
 */
export default class Analytics {
  async getMetrics(params: { period: string }) {
    return { metrics: [] };
  }
}
EOF

# 4. Update manifest (can be automated)
cat > .marketplace/photons.json << 'EOF'
{
  "name": "my-company-mcps",
  "photons": [
    {
      "name": "analytics",
      "version": "1.0.0",
      "description": "Company analytics queries",
      "source": "../analytics.photon.ts",
      "tools": ["getMetrics"]
    }
  ]
}
EOF

# 5. Commit and push
git add .
git commit -m "Add analytics MCP"
git push origin main
```

### Using Private Marketplace

```bash
# Add marketplace
photon marketplace add my-company/mcps

# Or with authentication for private repos
photon marketplace add https://github-token@github.com/my-company/mcps.git

# List and install
photon add analytics --marketplace my-company-mcps
```

---

## Integration Patterns

### Database Integration

```typescript
/**
 * @dependencies pg@^8.11.0
 */
import { Pool } from 'pg';

export default class Postgres {
  private pool: Pool;

  constructor(private connectionString: string) {
    this.pool = new Pool({ connectionString });
  }

  /**
   * Execute SQL query
   * @param sql SQL query to execute
   */
  async query(params: { sql: string }) {
    const result = await this.pool.query(params.sql);
    return {
      rows: result.rows,
      rowCount: result.rowCount,
    };
  }

  async onShutdown() {
    await this.pool.end();
  }
}
```

### REST API Integration

```typescript
/**
 * @dependencies axios@^1.6.0
 */
import axios, { AxiosInstance } from 'axios';

export default class APIClient {
  private client: AxiosInstance;

  constructor(
    private baseURL: string,
    private apiKey: string
  ) {
    this.client = axios.create({
      baseURL,
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      timeout: 30000,
    });
  }

  /**
   * GET request
   * @param endpoint API endpoint
   */
  async get(params: { endpoint: string }) {
    const response = await this.client.get(params.endpoint);
    return response.data;
  }

  /**
   * POST request
   * @param endpoint API endpoint
   * @param data Request body
   */
  async post(params: { endpoint: string; data: any }) {
    const response = await this.client.post(params.endpoint, params.data);
    return response.data;
  }
}
```

### External WebSocket Services

Connect to external WebSocket APIs (stock tickers, chat services, etc.):

> **Note:** This is for photons consuming external WebSocket services. Beam's internal architecture uses SSE via MCP Streamable HTTP—see [ARCHITECTURE.md](../core/ARCHITECTURE.md).

```typescript
/**
 * @dependencies ws@^8.16.0
 */
import WebSocket from 'ws';

export default class RealtimeData {
  private ws?: WebSocket;
  private messageQueue: any[] = [];

  constructor(private wsUrl: string) {}

  async onInitialize() {
    this.ws = new WebSocket(this.wsUrl);

    this.ws.on('message', (data) => {
      this.messageQueue.push(JSON.parse(data.toString()));
    });

    await new Promise((resolve, reject) => {
      this.ws!.on('open', resolve);
      this.ws!.on('error', reject);
    });
  }

  /**
   * Get latest messages
   */
  async getMessages() {
    const messages = [...this.messageQueue];
    this.messageQueue = [];
    return { messages };
  }

  async onShutdown() {
    this.ws?.close();
  }
}
```

---

## Best Practices Summary

### DO ✅

- Use TypeScript types for all parameters
- Add JSDoc comments for all tools
- Implement `onShutdown()` for resource cleanup
- Validate inputs before processing
- Use connection pooling for databases
- Cache expensive operations
- Log errors with context
- Test with real data
- Use semantic versioning

### DON'T ❌

- Store secrets in code
- Load large data at startup
- Use global mutable state
- Ignore errors
- Block the event loop
- Leave connections open
- Use synchronous APIs
- Hardcode URLs/endpoints
- Skip error handling

---

## Performance Targets

- **Startup time**: < 2 seconds
- **First request**: < 500ms
- **Subsequent requests**: < 100ms
- **Memory usage**: < 100MB for simple MCPs
- **Connection pool**: 5-20 connections
- **Cache hit rate**: > 80% for cacheable data

---

## Further Reading

- [MCP Protocol Specification](https://modelcontextprotocol.io/docs)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
- [Photon Examples](https://github.com/portel-dev/photon-examples)
