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 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.
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', ...]
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');
}
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.
Array of required environment variable keys
Throws
-
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' })
-
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']
The target object (class instance or prototype)
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');
}
The target object (class instance or prototype)
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 });
});
}
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:
- Fetches user’s environment variables for the project
- Wraps the tool execution in
runWithEnv()
- 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:
- Wrap code in
runWithEnv()
- 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