Skip to main content

Environment Injection

The @leanmcp/env-injection package provides secure, request-scoped environment variable injection for LeanMCP servers. It enables user-isolated access to secrets and configuration without global environment pollution.

Installation

npm install @leanmcp/env-injection reflect-metadata

Overview

Key Features

  • Request-scoped isolation: Each request has its own environment context
  • Async-safe: Uses AsyncLocalStorage to prevent race conditions
  • Type-safe: Full TypeScript support
  • Decorator-based: Simple @RequireEnv decorator for validation
  • Secure: Never exposes global environment variables

Use Cases

  • User-specific API keys (Slack token, GitHub token, etc.)
  • Multi-tenant configuration
  • Per-request secrets injection
  • Isolated test environments

API Reference

Context Functions

runWithEnv()

Run a function with environment variables in scope.
import { runWithEnv } from '@leanmcp/env-injection';

const result = await runWithEnv(
  { API_KEY: 'user-secret-key', API_URL: 'https://api.example.com' },
  async () => {
    // Code here has access to env vars via getEnv()
    return await fetchData();
  }
);
env
Record<string, string>
required
Environment variables to make available in the context
fn
() => T | Promise<T>
required
Function to execute with the environment context
return
T | Promise<T>
Return value from the function

getEnv()

Get an environment variable from the current request context.
import { getEnv } from '@leanmcp/env-injection';

const apiKey = getEnv('API_KEY');
const apiUrl = getEnv('API_URL');
This function requires an active environment context. It will throw an error if called outside of runWithEnv() or without @Authenticated decorator with projectId configured.
key
string
required
Environment variable key
return
string | undefined
The value or undefined if the key doesn’t exist

Throws

getEnv("API_KEY") called outside of env context.
To use getEnv(), you must configure 'projectId' in your @Authenticated decorator:
@Authenticated(authProvider, { projectId: 'your-project-id' })

getAllEnv()

Get all environment variables from the current request context.
import { getAllEnv } from '@leanmcp/env-injection';

const allEnvVars = getAllEnv();
console.log(Object.keys(allEnvVars)); // ['API_KEY', 'API_URL', ...]
return
Record<string, string>
A copy of all environment variables in the current context

Throws

Same error as getEnv() if called outside of environment context.

hasEnvContext()

Check if currently inside an environment context.
import { hasEnvContext } from '@leanmcp/env-injection';

if (hasEnvContext()) {
  const apiKey = getEnv('API_KEY');
} else {
  console.log('No env context available');
}
return
boolean
true if inside an environment context, false otherwise

Decorators

@RequireEnv()

Decorator to validate required environment variables exist before method execution.
import { Tool } from '@leanmcp/core';
import { RequireEnv, getEnv } from '@leanmcp/env-injection';

@Authenticated(authProvider, { projectId: 'my-project' })
class SlackService {
  @Tool({ description: 'Send Slack message' })
  @RequireEnv(['SLACK_TOKEN', 'SLACK_CHANNEL'])
  async sendMessage(args: { message: string }) {
    const token = getEnv('SLACK_TOKEN');
    const channel = getEnv('SLACK_CHANNEL');
    
    // Send message using user's token
    await slack.chat.postMessage({ token, channel, text: args.message });
    
    return { success: true };
  }
}
This decorator requires @Authenticated(authProvider, { projectId: '...' }) to be configured on the method or class. This is validated at runtime.
keys
string[]
required
Array of required environment variable keys

Throws

  1. If environment context is not configured:
    Environment injection not configured for SlackService.sendMessage().
    To use @RequireEnv, you must configure 'projectId' in your @Authenticated decorator:
    @Authenticated(authProvider, { projectId: 'your-project-id' })
    
  2. If required variables are missing:
    Missing required environment variables: SLACK_TOKEN, SLACK_CHANNEL.
    Please configure these secrets in your LeanMCP dashboard for this project.
    

getRequiredEnvKeys()

Get the required environment variable keys for a method.
import { getRequiredEnvKeys } from '@leanmcp/env-injection';

const keys = getRequiredEnvKeys(myService, 'sendMessage');
console.log(keys); // ['SLACK_TOKEN', 'SLACK_CHANNEL']
target
any
required
The target object (class instance or prototype)
propertyKey
string
required
Method name
return
string[] | undefined
Array of required keys, or undefined if decorator not applied

hasRequireEnv()

Check if a method has the @RequireEnv decorator.
import { hasRequireEnv } from '@leanmcp/env-injection';

if (hasRequireEnv(myService, 'sendMessage')) {
  console.log('Method requires environment variables');
}
target
any
required
The target object (class instance or prototype)
propertyKey
string
required
Method name
return
boolean
true if decorator is applied, false otherwise

Usage Examples

Basic Usage

import { runWithEnv, getEnv } from '@leanmcp/env-injection';

async function handleRequest(userId: string) {
  // Get user's environment variables from database
  const userEnv = await db.getUserEnv(userId);
  
  // Run with user's env context
  return await runWithEnv(userEnv, async () => {
    const apiKey = getEnv('API_KEY');
    return await externalApi.call({ apiKey });
  });
}

With MCP Tools

import { Tool } from '@leanmcp/core';
import { RequireEnv, getEnv } from '@leanmcp/env-injection';
import { Authenticated } from '@leanmcp/auth';

@Authenticated(authProvider, { projectId: 'slack-integration' })
class SlackService {
  @Tool({ description: 'List Slack channels' })
  @RequireEnv(['SLACK_TOKEN'])
  async listChannels() {
    const token = getEnv('SLACK_TOKEN');
    
    const response = await fetch('https://slack.com/api/conversations.list', {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    return await response.json();
  }
  
  @Tool({ description: 'Send message to Slack' })
  @RequireEnv(['SLACK_TOKEN', 'SLACK_CHANNEL'])
  async sendMessage(args: { message: string }) {
    const token = getEnv('SLACK_TOKEN');
    const channel = getEnv('SLACK_CHANNEL');
    
    await slack.postMessage({ token, channel, text: args.message });
    
    return { success: true };
  }
}

Multi-tenant Application

import { runWithEnv, getEnv } from '@leanmcp/env-injection';

class TenantService {
  async processTenantRequest(tenantId: string, action: string) {
    // Each tenant has their own environment
    const tenantEnv = await this.getTenantConfig(tenantId);
    
    return await runWithEnv(tenantEnv, async () => {
      const dbUrl = getEnv('DATABASE_URL');
      const apiKey = getEnv('EXTERNAL_API_KEY');
      
      // Process with tenant-specific configuration
      return await this.executeAction(action, { dbUrl, apiKey });
    });
  }
}

Conditional Environment Access

import { hasEnvContext, getEnv } from '@leanmcp/env-injection';

function getApiKey(): string {
  if (hasEnvContext()) {
    // Use request-scoped key if available
    return getEnv('API_KEY') ?? process.env.API_KEY!;
  }
  
  // Fall back to global environment
  return process.env.API_KEY!;
}

Testing

import { runWithEnv } from '@leanmcp/env-injection';

describe('MyService', () => {
  it('should use test environment', async () => {
    const testEnv = {
      API_KEY: 'test-key',
      API_URL: 'http://localhost:3000'
    };
    
    const result = await runWithEnv(testEnv, async () => {
      const service = new MyService();
      return await service.doSomething();
    });
    
    expect(result).toBeDefined();
  });
});

How It Works

AsyncLocalStorage

Under the hood, @leanmcp/env-injection uses Node.js’s AsyncLocalStorage to provide request-scoped storage:
import { AsyncLocalStorage } from 'async_hooks';

const envStorage = new AsyncLocalStorage<Record<string, string>>();

export function runWithEnv<T>(env: Record<string, string>, fn: () => T | Promise<T>) {
  return envStorage.run(env, fn);
}

export function getEnv(key: string): string | undefined {
  const store = envStorage.getStore();
  if (!store) throw new Error('...');
  return store[key];
}
This ensures:
  • Each async operation has its own isolated environment
  • No race conditions between concurrent requests
  • Automatic cleanup when request completes

Integration with @leanmcp/auth

When using @Authenticated with projectId, the auth middleware automatically:
  1. Fetches user’s environment variables for the project
  2. Wraps the tool execution in runWithEnv()
  3. Makes variables available via getEnv()
import { Authenticated } from '@leanmcp/auth';
import { RequireEnv, getEnv } from '@leanmcp/env-injection';

@Authenticated(authProvider, { projectId: 'my-project' })
class MyService {
  @Tool({ description: 'Example' })
  @RequireEnv(['API_KEY'])
  async example() {
    // Auth middleware already set up env context
    const apiKey = getEnv('API_KEY'); // Works!
    return { apiKey };
  }
}

Best Practices

1. Always Use with Authentication

Never expose raw environment injection without authentication:
// ✅ Good - secured with auth
@Authenticated(authProvider, { projectId: 'app' })
@RequireEnv(['SECRET_KEY'])

// ❌ Bad - anyone can access
@RequireEnv(['SECRET_KEY'])

2. Validate Required Variables

Use @RequireEnv to validate upfront:
// ✅ Good - fails fast with clear error
@RequireEnv(['API_KEY', 'API_SECRET'])

// ❌ Bad - fails later with unclear error
async myMethod() {
  const key = getEnv('API_KEY'); // might be undefined
  await api.call(key); // runtime error
}

3. Use hasEnvContext() for Optional Features

async function optionalFeature() {
  if (hasEnvContext()) {
    const feature = getEnv('OPTIONAL_FEATURE');
    if (feature) {
      return await enableFeature();
    }
  }
  return defaultBehavior();
}

4. Don’t Store References

Always call getEnv() when you need the value:
// ❌ Bad - stores reference outside context
class MyService {
  private apiKey = getEnv('API_KEY'); // Error!
}

// ✅ Good - retrieves when needed
class MyService {
  async myMethod() {
    const apiKey = getEnv('API_KEY');
  }
}

Troubleshooting

Error: “called outside of env context”

Cause: getEnv() called without active environment context. Solution:
  1. Wrap code in runWithEnv()
  2. Or add @Authenticated with projectId
// Option 1: Manual context
await runWithEnv(env, async () => {
  const key = getEnv('KEY');
});

// Option 2: Auth-based context
@Authenticated(provider, { projectId: 'app' })

Error: “Missing required environment variables”

Cause: Required env vars not present in context. Solution: Configure the variables in LeanMCP dashboard for your project.

TypeScript: “Cannot use decorators”

Enable decorators in tsconfig.json:
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

See Also

Build docs developers (and LLMs) love