@mcp-layer/manager
v0.2.0
Published
Session manager for building auth-aware MCP session pools with TTL/LRU eviction.
Maintainers
Readme
@mcp-layer/manager
Reusable MCP session manager with identity-based session reuse, TTL expiration, and LRU eviction.
This package is transport-agnostic. It can be used with REST plugins, CLIs, job workers, or any runtime that needs controlled MCP session lifecycles.
Table of Contents
- Installation
- What This Package Provides
- API Reference
- Identity Rules
- Example: Default Auth Parsing
- Example: Custom Identity Strategy
- Example: Integration with a Plugin
- Shutdown
- Runtime Error Reference
Installation
pnpm add @mcp-layer/manager
# or
npm install @mcp-layer/manager
# or
yarn add @mcp-layer/managerWhat This Package Provides
@mcp-layer/manager solves session lifecycle concerns that are separate from any single framework:
- Identity derivation per incoming request/context.
- Session reuse for repeated identities.
- Time-based eviction (
ttl) for stale sessions. - Capacity-based eviction (
max) using least-recently-used policy. - Graceful shutdown via
close(). - Runtime visibility via
stats().
API Reference
createManager(options)
Creates a manager instance.
createManager(options: {
max?: number;
ttl?: number;
sharedKey?: string;
auth?: {
mode?: 'optional' | 'required' | 'disabled';
header?: string;
scheme?: 'bearer' | 'basic' | 'raw';
};
identify?: (request) =>
| string
| {
key: string;
auth?: { scheme?: 'bearer' | 'basic' | 'raw'; token?: string; header?: string };
shared?: boolean;
};
factory: (ctx: {
identity: {
key: string;
auth: { scheme: 'bearer' | 'basic' | 'raw'; token: string; header: string } | null;
shared: boolean;
};
request: FastifyRequest;
}) => Promise<Session>;
now?: () => number;
}) => {
get(request): Promise<Session>;
stats(): {
size: number;
max: number;
ttl: number;
evictions: number;
hits: number;
misses: number;
keys: string[];
};
close(): Promise<void>;
}options fields
| Field | Type | Default | Required | Behavior |
| --- | --- | --- | --- | --- |
| max | number | 10 | no | Maximum cached sessions. When exceeded, oldest LRU session is evicted and closed. |
| ttl | number | 300000 | no | Idle timeout in milliseconds. Expired sessions are closed and recreated on next access. |
| sharedKey | string | "shared" | no | Identity key used when auth is optional and missing (or disabled). |
| auth.mode | 'optional' \| 'required' \| 'disabled' | 'optional' | no | Controls whether identity must come from auth headers. |
| auth.header | string | 'authorization' | no | Header name used for auth parsing. Case-insensitive. |
| auth.scheme | 'bearer' \| 'basic' \| 'raw' | 'bearer' | no | Header parsing strategy when identify is not provided. |
| identify | function | undefined | no | Custom identity derivation. Overrides built-in auth parsing. |
| factory | function | none | yes | Creates a Session for an identity. Must return @mcp-layer/session Session. |
| now | function | Date.now | no | Clock source, mainly for deterministic tests. |
Manager methods
| Method | Signature | Behavior |
| --- | --- | --- |
| get | get(request) => Promise<Session> | Resolves identity, returns cached session when possible, otherwise creates and caches a new session. |
| stats | stats() => { size, max, ttl, evictions, hits, misses, keys } | Returns in-memory pool statistics for observability and testing. |
| close | close() => Promise<void> | Closes all tracked sessions and clears in-memory state. |
Error Behavior
Manager runtime errors are thrown as LayerError from @mcp-layer/error.
- Every error includes package + method source metadata.
- Every error includes a stable
referenceid. - Every error includes a generated
docsURL to this package README error section. - Runtime references and full debugging playbooks are documented in Runtime Error Reference.
Identity Rules
When identify is not supplied, identity is derived from configured auth settings:
auth.mode = 'disabled': always usessharedKey.auth.mode = 'optional': uses auth header when present, otherwisesharedKey.auth.mode = 'required': missing header raises a documentedLayerErrorfromidentity.
Scheme handling:
bearer: expectsAuthorization: Bearer <token>.basic: expectsAuthorization: Basic <base64>.raw: takes the full header value as token.
Example: Default Auth Parsing
This example shows the default identity path (Authorization header) with per-identity session creation. This matters when multiple callers should not always share one MCP session.
Expected behavior: requests with the same bearer token reuse one session; a different token creates a different session.
import { createManager } from '@mcp-layer/manager';
import { connect } from '@mcp-layer/connect';
import { load } from '@mcp-layer/config';
const config = await load();
const manager = createManager({
max: 10,
ttl: 5 * 60 * 1000,
factory: async function factory(ctx) {
const entry = config.get('server-name');
if (!entry) {
throw new Error('Server not found.');
}
const token = ctx.identity.auth ? ctx.identity.auth.token : undefined;
return connect(config, entry.name, {
env: token ? { MCP_AUTH_TOKEN: token } : undefined
});
}
});Example: Custom Identity Strategy
This example shows tenant-based routing without forcing header auth parsing. This matters when identity comes from app-level metadata instead of authorization headers.
Expected behavior: requests with the same tenant key share one session; requests without tenant fallback to shared identity.
const manager = createManager({
identify: function identify(request) {
const tenant = request.headers['x-tenant-id'];
if (!tenant || typeof tenant !== 'string') {
return 'shared';
}
return {
key: `tenant:${tenant}`,
shared: false
};
},
factory: async function factory(ctx) {
return connect(config, 'tenant-server');
}
});Example: Integration with a Plugin
This example shows how to pass the manager into another package that resolves sessions per request.
Expected behavior: the host plugin calls manager.get(request) internally and reuses or creates sessions according to manager policy.
app.register(mcpRest, {
session,
manager
});Shutdown
Call close() during process shutdown so cached sessions are closed cleanly.
await manager.close();Runtime Error Reference
This section is written for high-pressure debugging moments. Each entry maps to a specific createManager(...) validation or identity-resolution branch.
factory must return a Session instance.
Thrown from: get
This happens when your factory(ctx) returns something other than @mcp-layer/session Session. Manager cache/storage and route integration require real Session instances.
Step-by-step resolution:
- Inspect the return value of
factory(ctx)and verify its constructor/type. - Ensure the factory awaits
connect(...)orattach(...)rather than returning raw clients. - Reject non-Session returns in your own factory wrapper.
- Add tests for one invalid factory return and one valid Session return.
const manager = createManager({
factory: async function makeSession() {
return connect(config, 'local-dev');
}
});
const session = await manager.get(request);Authorization header is required.
Thrown from: identity
This happens when auth.mode is set to required and the configured auth header is missing from the incoming request.
Step-by-step resolution:
- Confirm manager auth config (
mode,header,scheme) used at runtime. - Check upstream proxy/gateway forwarding for the authorization header.
- Ensure requests include the required header when manager auth is
required. - Add tests for missing-header rejection and valid-header acceptance.
await fastify.inject({
method: 'GET',
url: '/v1/tools/weather.get',
headers: { authorization: 'Bearer test-token' }
});Authorization header must use Basic scheme.
Thrown from: identity
This happens when manager auth scheme is configured as basic, but the request header is not formatted as Basic <base64>.
Step-by-step resolution:
- Verify manager auth config uses
scheme: "basic"intentionally. - Check header format and ensure it starts with
Basic. - Encode credentials as Base64 (
username:password) before sending. - Add tests for incorrect scheme prefix and valid Basic header parsing.
const credentials = Buffer.from('user:pass').toString('base64');
await fastify.inject({
method: 'GET',
url: '/v1/tools/example',
headers: { authorization: `Basic ${credentials}` }
});Authorization header must use Bearer scheme.
Thrown from: identity
This happens when manager auth scheme is bearer, but the header does not match Bearer <token>.
Step-by-step resolution:
- Confirm manager config uses
scheme: "bearer". - Check clients/proxies are not rewriting the header prefix.
- Ensure token is sent as
Bearer <token>exactly. - Add tests for malformed bearer headers and valid token headers.
await fastify.inject({
method: 'GET',
url: '/v1/tools/example',
headers: { authorization: 'Bearer abc123' }
});identify() must return a string or { key, auth } object.
Thrown from: identity
This happens when a custom identify(request) hook returns an unsupported shape (for example undefined, number, or object missing key).
Step-by-step resolution:
- Review your custom
identifyimplementation return type. - Return either a string key or
{ key, auth?, shared? }. - If supplying auth metadata, include
tokenunderauth. - Add tests for both supported return shapes.
const manager = createManager({
identify: function identify(request) {
const tenant = String(request.headers['x-tenant-id'] ?? 'shared');
return { key: `tenant:${tenant}`, shared: false };
},
factory: makeSession
});max must be a positive number.
Thrown from: normalize
This happens when createManager receives max <= 0, NaN, or non-finite values. max controls cache capacity and must be a positive number.
Step-by-step resolution:
- Inspect the source of
max(env/config flags). - Parse/coerce to number and validate positivity before manager creation.
- Set a sensible upper bound for your workload to avoid churn.
- Add tests for invalid (
0,-1,NaN) and valid values.
const max = Number(process.env.MCP_SESSION_MAX ?? 10);
if (!Number.isFinite(max) || max <= 0)
throw new Error('MCP_SESSION_MAX must be a positive number.');
const manager = createManager({ max, ttl: 300000, factory: makeSession });Session manager options are required.
Thrown from: normalize
This happens when createManager(...) is called with undefined, null, or a non-object value. Manager initialization requires an options object.
Step-by-step resolution:
- Check the code path building manager options.
- Ensure options object construction does not short-circuit to
undefined. - Add a local assertion before calling
createManager. - Add tests for missing-options and valid-options initialization.
const manager = createManager({
factory: makeSession,
max: 10,
ttl: 300000
});Session manager requires a factory function.
Thrown from: normalize
This happens when manager options do not include a callable factory. Session creation is delegated entirely to this function.
Step-by-step resolution:
- Verify
factoryexists and is a function. - Ensure dependency injection/config wiring does not pass factory results instead of function references.
- Keep factory async and return
Session. - Add tests that reject missing factory and accept valid factory functions.
const manager = createManager({
factory: async function makeSession(ctx) {
return connect(config, ctx.identity.key);
}
});ttl must be a positive number.
Thrown from: normalize
This happens when ttl is <= 0, NaN, or non-finite. Session entries use ttl for eviction; invalid values break expiration semantics.
Step-by-step resolution:
- Trace TTL input from environment/config to manager setup.
- Parse as number and enforce
ttl > 0. - Choose a TTL aligned with upstream session cost and traffic patterns.
- Add tests for invalid TTL values and expected expiration behavior.
const ttl = Number(process.env.MCP_SESSION_TTL_MS ?? 300000);
if (!Number.isFinite(ttl) || ttl <= 0)
throw new Error('MCP_SESSION_TTL_MS must be a positive number.');
const manager = createManager({ factory: makeSession, max: 10, ttl });License
MIT
