LeanMCP provides request-scoped environment variable injection, allowing each authenticated user to have their own secrets and configuration.
Quick Start
import { Authenticated , AuthProvider } from '@leanmcp/auth' ;
import { RequireEnv , getEnv } from '@leanmcp/env-injection' ;
import { Tool } from '@leanmcp/core' ;
const authProvider = new AuthProvider ( 'leanmcp' , {
apiKey: 'your-api-key'
});
@ Authenticated ( authProvider , { projectId: 'my-project' })
export class SlackService {
@ Tool ({ description: 'Send Slack message' })
@ RequireEnv ([ 'SLACK_TOKEN' , 'SLACK_CHANNEL' ])
async sendMessage ( args : { message : string }) {
// Get user's SLACK_TOKEN (not global!)
const token = getEnv ( 'SLACK_TOKEN' );
const channel = getEnv ( 'SLACK_CHANNEL' );
// Send message using user's credentials
await slack . chat . postMessage ({
token ,
channel ,
text: args . message
});
}
}
Environment injection requires the LeanMCP provider (@leanmcp/auth) and a projectId in the @Authenticated decorator.
How It Works
Enable environment injection by setting projectId in the @Authenticated decorator:
@ Authenticated ( authProvider , {
projectId: 'my-slack-integration' // Required for env injection
})
export class SlackService {
// ...
}
2. Use @RequireEnv Decorator
Validate that required environment variables exist before execution:
@ Tool ({ description: 'Create GitHub issue' })
@ RequireEnv ([ 'GITHUB_TOKEN' , 'GITHUB_REPO' ])
async createIssue ( args : { title: string ; body : string }) {
const token = getEnv ( 'GITHUB_TOKEN' );
const repo = getEnv ( 'GITHUB_REPO' );
// Create issue
}
Source: packages/env-injection/src/decorators.ts:30
If required environment variables are missing, the method throws an error: Missing required environment variables: GITHUB_TOKEN, GITHUB_REPO.
Please configure these secrets in your LeanMCP dashboard for this project.
3. Access Variables with getEnv()
Retrieve user-scoped environment variables:
import { getEnv , getAllEnv } from '@leanmcp/env-injection' ;
// Get single variable
const apiKey = getEnv ( 'OPENAI_API_KEY' );
// Get all variables
const allEnvs = getAllEnv ();
console . log ( 'Available secrets:' , Object . keys ( allEnvs ));
Source: packages/env-injection/src/env-context.ts:39
Request Isolation
Concurrency-Safe
Environment variables are stored in AsyncLocalStorage, ensuring each request has its own isolated context:
// Request A (User 1)
getEnv ( 'SLACK_TOKEN' ) // Returns User 1's token
// Request B (User 2) - concurrent
getEnv ( 'SLACK_TOKEN' ) // Returns User 2's token
Source: packages/env-injection/src/env-context.ts:8
This prevents race conditions in high-concurrency scenarios. Each user’s secrets are completely isolated.
How Isolation Works
User authenticates with JWT token
@Authenticated decorator fetches user secrets from LeanMCP API
Secrets are stored in AsyncLocalStorage for this request
getEnv() reads from request-scoped storage
Storage is cleaned up when request completes
Multi-Tenant Secrets
Per-User Configuration
Each user can configure their own secrets in the LeanMCP dashboard:
@ Authenticated ( authProvider , { projectId: 'slack-bot' })
export class SlackService {
@ Tool ({ description: 'Send message' })
@ RequireEnv ([ 'SLACK_TOKEN' ])
async sendMessage ( args : { message : string }) {
// Each user sends messages using their own Slack workspace
const token = getEnv ( 'SLACK_TOKEN' ); // User-specific token
await slack . sendMessage ( token , args . message );
}
}
User A’s secrets:
SLACK_TOKEN=xoxb-user-a-workspace
SLACK_CHANNEL=#general
User B’s secrets:
SLACK_TOKEN=xoxb-user-b-workspace
SLACK_CHANNEL=#random
Organization-Level Secrets
Scope secrets to organizations by including org context in the JWT:
@ Authenticated ( authProvider , { projectId: 'org-integration' })
export class OrgService {
@ Tool ({ description: 'Deploy to production' })
@ RequireEnv ([ 'AWS_ACCESS_KEY' , 'AWS_SECRET_KEY' ])
async deploy ( args : any ) {
// Uses organization's AWS credentials
const accessKey = getEnv ( 'AWS_ACCESS_KEY' );
const secretKey = getEnv ( 'AWS_SECRET_KEY' );
// Deploy using org credentials
}
}
Error Handling
Missing Configuration
If projectId is not configured:
@ Tool ({ description: 'Send message' })
@ RequireEnv ([ 'SLACK_TOKEN' ]) // ERROR: projectId not configured!
async sendMessage ( args : any ) {
// ...
}
Error message:
Environment injection not configured for SlackService.sendMessage().
To use @RequireEnv, you must configure 'projectId' in your @Authenticated decorator:
@Authenticated(authProvider, { projectId: 'your-project-id' })
Source: packages/env-injection/src/decorators.ts:40
Missing Variables
If required variables are not set by the user:
@ RequireEnv ([ 'API_KEY' , 'API_SECRET' ])
async apiCall ( args : any ) {
// ...
}
Error message:
Missing required environment variables: API_KEY, API_SECRET.
Please configure these secrets in your LeanMCP dashboard for this project.
Source: packages/env-injection/src/decorators.ts:52
Calling getEnv() Outside Context
If getEnv() is called without authentication:
// Outside @Authenticated method
const key = getEnv ( 'API_KEY' ); // ERROR!
Error message:
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' })
Source: packages/env-injection/src/env-context.ts:42
Advanced Usage
Optional Environment Variables
Use getEnv() without @RequireEnv for optional variables:
@ Authenticated ( authProvider , { projectId: 'my-project' })
export class MyService {
@ Tool ({ description: 'Send notification' })
async notify ( args : { message : string }) {
// Optional: use Slack if configured
const slackToken = getEnv ( 'SLACK_TOKEN' );
if ( slackToken ) {
await sendSlackMessage ( slackToken , args . message );
}
// Always send email
await sendEmail ( args . message );
}
}
Combining with Authentication
Access both user info and environment variables:
@ Authenticated ( authProvider , { projectId: 'integration' })
export class IntegrationService {
@ Tool ({ description: 'Sync data' })
@ RequireEnv ([ 'API_KEY' ])
async syncData ( args : any ) {
// Access user info
const userId = authUser . sub ;
const userEmail = authUser . email ;
// Access user secrets
const apiKey = getEnv ( 'API_KEY' );
// Sync data for this user
await api . sync ( userId , apiKey );
}
}
Dynamic Environment Variables
Build variable names dynamically:
@ RequireEnv ([ 'OPENAI_API_KEY' , 'ANTHROPIC_API_KEY' ])
async callAI ( args : { provider: 'openai' | 'anthropic' ; prompt : string }) {
// Select API key based on provider
const envKey = ` ${ args . provider . toUpperCase () } _API_KEY` ;
const apiKey = getEnv ( envKey );
// Call appropriate AI service
return callAIService ( args . provider , apiKey , args . prompt );
}
LeanMCP Provider Requirement
Environment injection only works with the LeanMCP authentication provider: // ✅ Correct: LeanMCP provider
const authProvider = new AuthProvider ( 'leanmcp' , {
apiKey: 'your-api-key'
});
// ❌ Won't work: Other providers don't support user secrets
const authProvider = new AuthProvider ( 'auth0' , {
domain: 'example.auth0.com'
});
Other providers (Auth0, Clerk, Cognito) only support authentication, not user secret management.
Source: packages/auth/src/decorators.ts:248
Context API
runWithEnv()
Manually run code with environment context:
import { runWithEnv , getEnv } from '@leanmcp/env-injection' ;
const userSecrets = {
'API_KEY' : 'user-key-123' ,
'API_SECRET' : 'user-secret-456'
};
await runWithEnv ( userSecrets , async () => {
const key = getEnv ( 'API_KEY' );
console . log ( 'Key:' , key ); // 'user-key-123'
});
Source: packages/env-injection/src/env-context.ts:15
hasEnvContext()
Check if currently in an environment context:
import { hasEnvContext } from '@leanmcp/env-injection' ;
if ( hasEnvContext ()) {
const key = getEnv ( 'API_KEY' );
} else {
console . log ( 'No env context available' );
}
Source: packages/env-injection/src/env-context.ts:24
Best Practices
1. Always Use @RequireEnv for Critical Secrets
// Good: Validates before execution
@ RequireEnv ([ 'DATABASE_URL' , 'ENCRYPTION_KEY' ])
async processData ( args : any ) {
const dbUrl = getEnv ( 'DATABASE_URL' );
// ...
}
// Bad: Might fail at runtime
async processData ( args : any ) {
const dbUrl = getEnv ( 'DATABASE_URL' ); // Might be undefined!
}
2. Use Descriptive Variable Names
// Good
@ RequireEnv ([ 'STRIPE_SECRET_KEY' , 'STRIPE_WEBHOOK_SECRET' ])
// Bad
@ RequireEnv ([ 'KEY1' , 'KEY2' ])
3. Document Required Variables
Document which secrets users need to configure:
/**
* Send Slack messages.
*
* Required environment variables:
* - SLACK_TOKEN: Bot token from https://api.slack.com/apps
* - SLACK_CHANNEL: Default channel ID (e.g., C01234567)
*/
@ Tool ({ description: 'Send Slack message' })
@ RequireEnv ([ 'SLACK_TOKEN' , 'SLACK_CHANNEL' ])
async sendMessage ( args : { message: string }) {
// ...
}
4. Handle Missing Optional Variables
async notify ( args : { message: string }) {
// Check before using
const slackToken = getEnv ( 'SLACK_TOKEN' );
if ( slackToken ) {
await sendSlack ( slackToken , args . message );
} else {
console . log ( 'Slack not configured, skipping notification' );
}
}
Next Steps
Authentication Set up LeanMCP authentication
Multi-Tenancy Per-user data isolation