@aperovn/mcp-context
v0.0.1
Published
Shared ABAC policy types, context injection/extraction, and evaluator for the Apero VN MCP gateway ecosystem
Maintainers
Readme
@aperovn/mcp-context
Shared ABAC policy types, context injection/extraction, and evaluator for the Apero VN MCP gateway ecosystem.
Trust model: internal VPC only. Downstream MCPs trust gateway headers because they are not publicly reachable. If this assumption changes, signing must be reintroduced.
- ABAC policy evaluation engine (attribute-based access control)
- Lazy policy fetch with 30-second cache
- Dual ESM/CJS build, zero crypto dependencies
Installation
bun add @aperovn/mcp-context
# or
npm install @aperovn/mcp-contextQuick Start
Downstream MCP Integration (~5 LOC)
import { resolveContext, canPerform } from '@aperovn/mcp-context';
// Option 1: Explicit gateway URL
const ctx = await resolveContext(req.headers, 'http://gateway:3000');
// Option 2: Via env var (GATEWAY_INTERNAL_URL=http://gateway:3000)
const ctx = await resolveContext(req.headers);
// Use in business logic
const decision = canPerform(ctx, 'write:file', 'files/data.txt', 'filesystem');
if (decision.effect === 'deny') {
return res.status(403).json({ error: decision.reason });
}Gateway Integration
import { injectContext } from '@aperovn/mcp-context';
const headers: Record<string, string> = {};
injectContext(userContext, headers);
// Forward request to downstream MCP
fetch('http://downstream-mcp/mcp', { headers });Wire Protocol
Headers
X-API-USER: [email protected]
X-API-TEAMS: mobile,finance (empty string if none)
X-API-ADMIN: true|falseThree headers. No version field. No timestamp. No signature. Header names are exported as constants — consume them instead of hardcoding literals:
import { HEADER_USER, HEADER_TEAMS, HEADER_ADMIN } from '@aperovn/mcp-context';Note: Policies are not forwarded in headers — fetched lazily via resolveContext.
Size limits
- No header size limits enforced in v2.0.0
- Policies cached with 30-second TTL in downstream MCPs
- Network pressure eliminated through lazy loading
API Reference
Types
interface Policy {
id?: number;
name?: string;
effect: 'allow' | 'deny';
principal_type: 'user' | 'team' | '*';
principal_id: string;
actions: string[]; // Glob patterns (empty = fail-closed)
mcp_scope: string[]; // MCP names (empty = fail-closed)
resources: string[]; // Glob patterns (empty = fail-closed)
priority: number; // 0-1000
}
interface UserContext {
email: string;
teams: string[];
isAdmin: boolean;
policies: Policy[]; // Pre-sorted by priority
}
interface Decision {
effect: 'allow' | 'deny';
matchedPolicies: Policy[];
reason: string;
}Functions
injectContext(user, headers)
Inject user context into HTTP headers (mutates headers in place).
- user:
UserContext - headers:
Headers | Record<string, string>(mutated in-place)
extractContext(headers) → UserContext
Extract user context from HTTP headers. Returns frozen UserContext with policies: []. Throws InvalidContextError on missing X-API-USER.
Use resolveContext() when you need actual policies.
resolveContext(headers, gatewayUrl?) → Promise<UserContext>
Extract user context and fetch policies lazily from gateway.
- headers:
Headers | Record<string, string> - gatewayUrl:
string(optional) — Gateway base URL. Falls back toGATEWAY_INTERNAL_URLenv var. Throws if neither provided.
Returns frozen UserContext with policies. Uses 30-second cache. Throws InvalidContextError on failures.
canPerform(ctx, action, resource, mcp?) → Decision
Evaluate ABAC policies for a specific resource operation.
- ctx:
UserContext - action:
string(e.g.,"read:file") - resource:
string(e.g.,"files/data.txt") - mcp:
string(optional, MCP name)
canCallTool(ctx, action, mcp?) → 'allowed' | 'denied' | 'conditionally-allowed'
Tool listing mode (resource-agnostic).
- Returns
'conditionally-allowed'if policies have resource restrictions - Returns
'allowed'if unrestricted allow - Returns
'denied'if deny or no match
hasDeny(ctx, action, resource?, mcp?) → boolean
Fast deny check (short-circuits on first matching deny policy).
PolicyCache
In-memory TTL cache for policy responses (30-second expiry).
PolicyClient
HTTP client for fetching policies from gateway internal endpoint.
policySchema
JSON Schema for Policy validation (Ajv-compatible).
import Ajv from 'ajv';
import { policySchema } from '@aperovn/mcp-context';
const ajv = new Ajv();
const validatePolicy = ajv.compile(policySchema);Migration Guide (from internal pre-publish version)
For Downstream MCPs
Before (internal):
import { extractContext } from '@aperovn/mcp-context';
// Extract context with policies
const ctx = extractContext(req.headers);
// ctx.policies contains actual policy array
// Use in business logic
const decision = canPerform(ctx, 'write:file', 'files/data.txt', 'filesystem');After (public):
import { resolveContext } from '@aperovn/mcp-context';
// Extract context and fetch policies lazily
// gatewayUrl optional — uses GATEWAY_INTERNAL_URL env var if omitted
const ctx = await resolveContext(req.headers);
// ctx.policies fetched from gateway with 30s cache
// Use in business logic (same as before)
const decision = canPerform(ctx, 'write:file', 'files/data.txt', 'filesystem');Required Environment Variable:
GATEWAY_INTERNAL_URL=http://gateway:3000For Gateway (Internal Change)
Before:
import { injectContext } from '@aperovn/mcp-context';
injectContext(ctx, ctx.policies, headers);After:
import { injectContext } from '@aperovn/mcp-context';
injectContext(ctx, headers); // policies parameter removedKey Changes Summary
X-API-POLICIESheader no longer injectedpoliciesparameter removed frominjectContextresolveContext()async API for lazy policy fetching- 30-second cache reduces network calls
extractContext()returns empty policies array
Migration Steps
- Add
GATEWAY_INTERNAL_URLenvironment variable to downstream MCPs - Replace
extractContext()calls withawait resolveContext() - Update
injectContext()calls in gateway (remove policies parameter) - Deploy gateway first, then downstream MCPs
Policy Semantics
- Empty arrays = fail-closed:
[]matches nothing (explicit deny) - Wildcards:
['*']matches all - Priority: Higher wins (0-1000)
- Tie-breaks: Deny beats allow at same priority
- Admin bypass:
isAdmin: trueignores all policies - Glob patterns: Minimatch syntax (
**/*,projects/*,com.apero.*)
Examples
Allow mobile team to read files
{
"effect": "allow",
"principal_type": "team",
"principal_id": "mobile",
"actions": ["read:*", "list:*"],
"mcp_scope": ["filesystem"],
"resources": ["files/mobile/*"],
"priority": 100
}Deny prod writes for non-admins
{
"effect": "deny",
"principal_type": "*",
"principal_id": "*",
"actions": ["write:*", "delete:*"],
"mcp_scope": ["*"],
"resources": ["files/prod/*"],
"priority": 1000
}License
MIT
Version History
See CHANGELOG.md
