CLAUDE.md@libs/plugins · git:20260106.b334103 · 2026-01-06 · sha256 a682b9dab83bffda

CLAUDE.md@libs/plugins git:20260106.b334103A

Immutable. This exact content never changes and is served at /api/v1/blob/a682b9dab83bffda.

# @frontmcp/plugins - Development Guidelines

## Overview

`@frontmcp/plugins` provides extensible plugins for the FrontMCP SDK, including caching, code execution, dashboard, and session memory (RememberPlugin).

## Plugin Architecture

### Creating a DynamicPlugin

Plugins should extend `DynamicPlugin<Options, OptionsInput>` for configurable behavior:

```typescript
import { DynamicPlugin, Plugin, ProviderType } from '@frontmcp/sdk';

@Plugin({
  name: 'my-plugin',
  description: 'Description',
  providers: [
    /* static providers */
  ],
})
export default class MyPlugin extends DynamicPlugin<MyPluginOptions, MyPluginOptionsInput> {
  static defaultOptions: MyPluginOptions = {
    /* defaults */
  };

  static override dynamicProviders = (options: MyPluginOptionsInput): ProviderType[] => {
    // Return providers based on options
    return [];
  };
}
```

### Extending Tool Metadata

Plugins can extend tool metadata using `declare global` pattern. This allows tools to specify plugin-specific options in their decorators:

```typescript
// In your plugin's types file
declare global {
  interface ExtendFrontMcpToolMetadata {
    myOption?: MyOptionType | boolean;
  }
}
```

This enables usage like:

```typescript
@Tool({
  name: 'my_tool',
  myOption: { enabled: true }, // TypeScript knows about this!
})
class MyTool extends ToolContext { ... }
```

### Accessing Plugin Services via Context Extensions

Plugins can extend ExecutionContextBase with new properties like `this.remember` using the SDK's context extension mechanism.

**Two parts are required:**

1. **TypeScript types** (declared by plugin developer via `declare module`)
2. **Runtime installation** (handled by SDK via `contextExtensions` in plugin metadata)

**Pattern used by RememberPlugin:**

```typescript
// 1. TypeScript types (in context-extension.ts)
declare module '@frontmcp/sdk' {
  interface ExecutionContextBase {
    readonly myService: MyService;
  }
}

// 2. Plugin metadata with contextExtensions (in plugin.ts)
@Plugin({
  name: 'my-plugin',
  contextExtensions: [
    {
      property: 'myService',
      token: MyServiceToken,
      errorMessage: 'MyPlugin is not installed.',
    },
  ],
})
class MyPlugin extends DynamicPlugin { ... }
```

**How it works:**

- Plugin declares `contextExtensions` array in metadata
- SDK processes this during plugin registration
- SDK adds lazy getters to `ExecutionContextBase.prototype`
- Plugin does NOT directly access SDK internals

**Usage in tools:**

```typescript
// this.myService is now available when MyPlugin is installed
class MyTool extends ToolContext {
  async execute(input) {
    await this.myService.doSomething();
  }
}
```

### Crypto Utilities

**IMPORTANT**: Always use `@frontmcp/utils` for cryptographic operations. Never use `node:crypto` directly.

```typescript
import {
  hkdfSha256, // HKDF-SHA256 key derivation
  encryptAesGcm, // AES-256-GCM encryption
  decryptAesGcm, // AES-256-GCM decryption
  randomBytes, // Cryptographic random bytes
  sha256,
  sha256Hex, // SHA-256 hashing
  base64urlEncode, // Base64url encoding
  base64urlDecode, // Base64url decoding
} from '@frontmcp/utils';
```

This ensures cross-platform support (Node.js and browser).

## RememberPlugin

### Usage in Tools

When RememberPlugin is installed, `this.remember` and `this.approval` are automatically available on any execution context (ToolContext, AgentContext, etc.):

```typescript
@Tool({ name: 'my_tool' })
class MyTool extends ToolContext {
  async execute(input) {
    // Direct property access - available when plugin is installed
    // Throws clear error if plugin not installed

    // Store a value (session-scoped by default)
    await this.remember.set('theme', 'dark');

    // Retrieve a value
    const theme = await this.remember.get('theme', { defaultValue: 'light' });

    // Store with user scope (persists across sessions)
    await this.remember.set('language', 'en', { scope: 'user' });

    // Store with TTL (forgets after 5 minutes)
    await this.remember.set('temp_token', 'xyz', { ttl: 300 });

    // Check if something is remembered
    if (await this.remember.knows('onboarding_complete')) {
      // Skip onboarding
    }

    // Forget something
    await this.remember.forget('temp_token');

    // List remembered keys
    const keys = await this.remember.list({ pattern: 'user:*' });

    // Check tool approval (if approval is enabled)
    const isApproved = await this.approval.isApproved('dangerous-tool');
  }
}
```

### Alternative: Helper Functions

If you prefer explicit function calls or need graceful degradation:

```typescript
import { getRemember, tryGetRemember } from '@frontmcp/plugins/remember';

class MyTool extends ToolContext {
  async execute(input) {
    // Option 1: Explicit function call (same as this.remember)
    const remember = getRemember(this);
    await remember.set('key', 'value');

    // Option 2: Safe access (returns undefined if plugin not installed)
    const maybeRemember = tryGetRemember(this);
    if (maybeRemember) {
      await maybeRemember.set('key', 'value');
    }
  }
}
```

### Scopes

- `session` - Valid only for current session (default)
- `user` - Persists for user across sessions
- `tool` - Tied to specific tool + session
- `global` - Shared across all sessions/users

### Approval System

Configure approval requirements on tools:

```typescript
@Tool({
  name: 'file_write',
  approval: {
    required: true,
    defaultScope: 'session',
    category: 'write',
    riskLevel: 'medium',
    approvalMessage: 'Allow file writing for this session?',
  },
})
class FileWriteTool extends ToolContext { ... }
```

## Testing Requirements

- **Coverage**: 95%+ across all metrics
- **Unit Tests**: Test all providers, services, and utilities
- **Mock Store**: Use `MockStore` implementing `RememberStoreInterface` for tests

## Anti-Patterns to Avoid

- **Using `node:crypto` directly** - use `@frontmcp/utils` for cross-platform support
- **Module-level side effects for context extension** - causes circular dependencies
- **Using `any` type** without justification
- **Hardcoding encryption keys** - use environment variables
- **Missing `override` keyword** on inherited methods
- **Exposing internal error details** in public messages

### Correct vs. Incorrect Patterns

```typescript
// ❌ WRONG: Side effect at module load (causes circular deps)
// At top level of module:
installExtension(); // Called immediately when module loads

// ✅ CORRECT: Extension in plugin constructor
class MyPlugin extends DynamicPlugin {
  constructor(options) {
    super();
    installExtension(); // Called when plugin is instantiated
  }
}

// ❌ WRONG: Using require() for SDK imports
const sdk = require('@frontmcp/sdk');
const ExecutionContextBase = sdk.ExecutionContextBase;

// ✅ CORRECT: Proper ES module imports
import { ExecutionContextBase } from '@frontmcp/sdk';

// ✅ CORRECT: Module augmentation for context properties (with runtime extension)
declare module '@frontmcp/sdk' {
  interface ExecutionContextBase {
    readonly myService: MyService;
  }
}
// Runtime: install in plugin constructor, not module side effect

// ✅ CORRECT: Module augmentation for tool metadata
declare global {
  interface ExtendFrontMcpToolMetadata {
    myOption?: MyOptionType;
  }
}
```