Skip to content

Advanced Photon Guide ​

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

Table of Contents ​


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 &lt;name&gt;.


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.

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 ​

Released under the MIT License.