-
Notifications
You must be signed in to change notification settings - Fork 11
Description
Product Requirements Document: Template Variable Substitution for MCP Funnel
Executive Summary
This PRD outlines the implementation of a template variable substitution system for MCP Funnel configurations. This feature will enable users to reference secrets and environment variables using template syntax (e.g., ${secret:GITHUB_TOKEN}) throughout their configuration files, eliminating the need to hardcode sensitive values and improving configuration portability.
Problem Statement
Current Limitations
-
No Variable Substitution: Currently, MCP Funnel's
secretProviderssystem only makes environment variables available to spawned processes. There's no way to reference these secrets in other configuration fields. -
Hardcoded Secrets: Users must hardcode sensitive values directly in configuration files or use workarounds like Docker's
--env-fileflag. -
Limited HTTP Authentication Support: Remote MCP servers requiring authentication headers cannot securely reference tokens from secret providers.
-
Configuration Portability: Configurations cannot be shared between team members or environments without manual token replacement.
Evidence from Codebase
- No template substitution logic exists in
packages/mcp/src/config.ts - Server spawning in
packages/mcp/src/index.ts:831-835passes configuration values as-is - Test file
packages/mcp/src/registry/config-generator.test.ts:line-with-${API_TOKEN}shows placeholder syntax but no actual implementation - Config generator documentation mentions "Replace placeholder values" as a manual step (
packages/mcp/src/registry/config-generator.ts)
User Stories
Story 1: GitHub API Authentication
As a developer using GitHub's hosted MCP server
I want to reference my GitHub token from a .env file in the authorization header
So that I don't have to hardcode my token in the configuration
{
"servers": {
"github": {
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer ${secret:GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
},
"secretProviders": [
{ "type": "dotenv", "config": { "path": ".env" } }
]
}Story 2: Dynamic Docker Environment Variables
As a developer using Docker-based MCP servers
I want to dynamically pass environment variables to Docker containers
So that I can use MCP Funnel's secret management instead of Docker's --env-file
{
"servers": {
"custom-server": {
"command": "docker",
"args": [
"run",
"-e",
"API_KEY=${secret:API_KEY}",
"-e",
"DB_URL=${secret:DATABASE_URL}",
"-i",
"--rm",
"my-mcp-server"
],
"secretProviders": [
{ "type": "dotenv", "config": { "path": ".env.production" } }
]
}
}
}Story 3: Environment-Specific Configuration
As a DevOps engineer
I want to use environment variables to configure server endpoints
So that the same configuration works across dev, staging, and production
{
"servers": {
"api": {
"url": "${env:API_ENDPOINT:-https://api.example.com}/mcp",
"headers": {
"X-Environment": "${env:ENVIRONMENT:-development}"
}
}
}
}Story 4: File-Based Secrets
As a security-conscious developer
I want to read secrets from files (e.g., Kubernetes mounted secrets)
So that I can integrate with existing secret management systems
{
"servers": {
"secure-server": {
"command": "node",
"args": ["server.js"],
"env": {
"LICENSE_KEY": "${file:/var/secrets/license.txt}",
"CERT_CONTENT": "${file:/etc/ssl/cert.pem}"
}
}
}
}Proposed Solution
Template Syntax
${source:key} - Basic substitution
${source:key:-default} - With default value
${source:key:?error} - Error if not found
Shell-style references like $VAR are parsed as sugar for ${env:VAR}, keeping the canonical ${source:key} syntax while supporting existing configs.
Supported Sources
-
secret:- Values from resolved secret providers- Example:
${secret:API_KEY} - Resolution: Looks up in secrets resolved via
secretProviders
- Example:
-
env:- Environment variables from current process- Example:
${env:HOME} - Resolution: Reads from
process.env
- Example:
-
file:- Contents from files- Example:
${file:/path/to/secret} - Resolution: Reads file contents, trims whitespace
- Example:
-
config:- Cross-references within configuration- Example:
${config:servers.github.url} - Resolution: JSON path lookup within current config
- Example:
Implementation Architecture
1. Template Resolution Phase
Create new module: packages/mcp/src/template-resolver.ts
export interface TemplateContext {
secrets: Record<string, string>; // From secretProviders
env: Record<string, string>; // From process.env
config: ProxyConfig; // Current configuration
}
export class TemplateResolver {
constructor(private context: TemplateContext) {}
async resolve(value: string): Promise<string> {
// Regex: /\$\{([^:}]+):([^}]+)\}/g
// Parse source and key
// Apply resolution based on source type
// Handle defaults and errors
}
async resolveObject(obj: any): Promise<any> {
// Recursively resolve all string values in object
}
}2. Integration Points
Configuration Loading (packages/mcp/src/config-loader.ts):
// After loading config
const secrets = await resolveSecretsFromConfig(config.defaultSecretProviders);
const resolver = new TemplateResolver({ secrets, env: process.env, config });
const resolvedConfig = await resolver.resolveObject(config);Server Connection (packages/mcp/src/index.ts:761-810):
private async resolveServerEnvironment(targetServer: TargetServer) {
// ... existing secret resolution ...
// NEW: Apply template resolution to server config
const resolver = new TemplateResolver({
secrets: finalEnv,
env: process.env,
config: this._config
});
targetServer = await resolver.resolveObject(targetServer);
return finalEnv;
}3. Security Considerations
- No Logging: Never log resolved template values
- Validation: Validate file paths to prevent directory traversal
- Permissions: Check file read permissions before accessing
- Local files only:
file:source resolves paths on the local filesystem; remote URLs are out of scope for the MVP - Size limits: Reject files larger than the configurable maximum (default 1 MB) to mitigate DoS vectors
- Escaping: Properly escape resolved values for shell commands
Architecture & Type Contracts
Define explicit interfaces so additional sources slot in without refactoring core logic:
export interface TemplateSourceResolver {
readonly source: TemplateSourceType;
resolve(token: TemplateToken, ctx: TemplateResolutionContext): Promise<string>;
canResolve(token: TemplateToken): boolean;
}
export interface TemplateResolutionContext {
readonly secrets: Record<string, string>;
readonly env: NodeJS.ProcessEnv;
readonly config: Record<string, unknown>;
readonly resolver: TemplateResolver;
}
export interface TemplateToken {
raw: string;
source: TemplateSourceType;
key: string;
defaultValue?: string;
errorMessage?: string;
}
export type TemplateSourceType = 'secret' | 'env' | 'file' | 'config';TemplateResolver maintains a registry of TemplateSourceResolver instances. New sources register through a small factory so future additions (e.g., vault:) are additive.
Configuration Schema Updates
Update packages/mcp/src/config.ts:
// Add template validation to string fields
const TemplateStringSchema = z.string().refine(
(val) => {
// Validate template syntax if present
const templateRegex = /\$\{([^:}]+):([^}]+)\}/g;
// Check for valid sources: secret, env, file, config
return true; // Actual validation logic
},
{ message: "Invalid template syntax" }
);
// Update schemas to use TemplateStringSchema where appropriate
export const TargetServerSchema = z.object({
name: z.string(),
command: TemplateStringSchema,
args: z.array(TemplateStringSchema).optional(),
// ... etc
});Testing Requirements
Unit Tests
Location: packages/mcp/src/template-resolver.test.ts
- Basic substitution:
${secret:KEY}→ resolved value - Default values:
${env:MISSING:-default}→ "default" - Error handling:
${secret:MISSING:?Required}→ throws error - Nested templates:
${secret:${env:KEY_NAME}}→ resolved - Escaping:
\${literal}→ "${literal}" - File reading: Mock file system for file: source tests
- Shell alias:
$MY_VARresolves identically to${env:MY_VAR}
Integration Tests
Location: packages/mcp/src/secrets/template-integration.test.ts
- End-to-end flow: Config with templates → resolved server spawn
- Docker args: Verify
-e KEY=valuecorrectly substituted - HTTP headers: Verify Authorization header substitution
- Multiple providers: Test precedence with multiple secret sources
- Server env merge: Ensure template expansion composes with resolved passthrough env (e.g., Windows PATH)
- Error scenarios: Missing required values, invalid syntax
Security Tests
- Path traversal:
${file:../../etc/passwd}→ blocked - Command injection:
${env:USER;rm -rf /}→ safely escaped - Circular references:
${config:a}→${config:b}→${config:a}→ detected - File size guard: Files larger than limit trigger validation error
Implementation Plan
- Core resolver
- Implement
TemplateResolverwith token parsing, default/error handling, and registry ofTemplateSourceResolvers. - Add concrete resolvers for
secret,env,file, andconfigsources; include shell alias handling in theenvresolver.
- Implement
- Configuration wiring
- Update
config-loaderto run the resolver across loaded configs before normalization. - Extend schema types (
TemplateStringSchema) so template syntax validation happens during parse.
- Update
- Runtime integration
- Apply template resolution to server spawn paths (
resolveServerEnvironment, command args, headers). - Ensure resolved values merge cleanly with filtered passthrough env on all platforms.
- Apply template resolution to server spawn paths (
- File access safeguards
- Enforce local path resolution, size limits, and permission checks in the
fileresolver.
- Enforce local path resolution, size limits, and permission checks in the
- Testing
- Unit tests for resolver/token parsing, default/error branches, shell alias, and each source.
- Integration tests covering Docker args, HTTP headers, env merge (including Windows PATH), and failure scenarios.
- Security tests for traversal, command injection, circular references, and oversize files.
- Documentation
- Update README and example configs to show template usage.
- Add notes on security best practices and file-source constraints.
Alternative Solutions Considered
1. Environment Variable Expansion Only
- Pros: Simpler, follows shell conventions
- Cons: Limited to env vars, no secret provider integration
- Decision: Rejected - doesn't leverage secretProviders system
2. JavaScript Expression Evaluation
- Pros: More flexible, supports complex logic
- Cons: Security risks, complexity, performance overhead
- Decision: Rejected - too complex and risky
3. External Preprocessing Tool
- Pros: Separation of concerns, language agnostic
- Cons: Additional dependency, complex deployment
- Decision: Rejected - poor developer experience
Resolved Questions
- Async resolution scope:
file:remains local-only; remote URLs are explicitly out of scope for the MVP to avoid network exfiltration paths. - Caching policy: Skip resolver caching for now—configuration payloads are small, and eager caching complicates invalidation without clear benefit.
- Syntax sugar: Support bash-style
$VARas a direct alias for${env:VAR}while keeping${source:key}as the canonical form. - Validation timing: Perform syntax validation during config load; runtime errors surface only when referenced values are actually missing.
Appendix
A. Current Code References
Files requiring modification:
packages/mcp/src/config.ts- Schema updatespackages/mcp/src/config-loader.ts- Template resolution integrationpackages/mcp/src/index.ts:761-810- Server environment resolutionpackages/mcp/src/index.ts:831-835- Server spawning
New files to create:
packages/mcp/src/template-resolver.ts- Core resolution logicpackages/mcp/src/template-resolver.test.ts- Unit testspackages/mcp/src/secrets/template-integration.test.ts- Integration tests
B. Example Configurations
Before (Current)
{
"servers": {
"github": {
"command": "docker",
"args": ["run", "--env-file", ".env", "-i", "--rm", "ghcr.io/github/github-mcp-server"]
}
}
}After (With Templates)
{
"servers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "${secret:GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
},
"secretProviders": [
{ "type": "dotenv", "config": { "path": ".env" } }
]
}C. Compatibility Matrix
| Feature | Current | With Templates | Breaking Change |
|---|---|---|---|
| Hardcoded values | ✅ | ✅ | No |
| Environment variables | Via spawn env | ${env:VAR} |
No |
| Secret providers | Process env only | ${secret:KEY} |
No |
| Docker --env-file | Required | Optional | No |
| HTTP headers | Hardcoded only | ${secret:TOKEN} |
No |
| File contents | Not supported | ${file:path} |
No |