Skip to main content

Server Adapter Configuration Reference

This document provides a comprehensive reference for all configuration options available in NeuroLink Server Adapters.

Configuration via CLI​

In addition to programmatic configuration, NeuroLink provides CLI commands to view and manage server settings.

Viewing Configuration​

# Show all configuration
neurolink server config

# Output as JSON
neurolink server config --format json

# Get specific value
neurolink server config --get defaultPort
neurolink server config --get cors.enabled
neurolink server config --get rateLimit.maxRequests

Modifying Configuration​

# Set configuration values
neurolink server config --set defaultPort=8080
neurolink server config --set defaultFramework=express
neurolink server config --set cors.enabled=true
neurolink server config --set rateLimit.maxRequests=200

# Reset to defaults
neurolink server config --reset

Configuration File Location​

CLI configuration is stored at:

  • Config file: ~/.neurolink/server-config.json
  • Server state: ~/.neurolink/server-state.json

CLI vs Programmatic Configuration​

AspectCLI ConfigProgrammatic Config
PersistenceFile-based, survives restartsIn-memory, per-instance
ScopeGlobal defaultsPer-server instance
Use CaseDevelopment, quick changesProduction, fine-grained control

neurolink serve resolves each setting from, in order: its command-line flag, the --config JSON file, then the built-in default. The listen port additionally reads the PORT environment variable after the config file, which is what container platforms set:

SettingOrder
Port--port → config port → $PORT → 3000
Host--host → config host → 127.0.0.1
Framework--framework → config framework → hono
Base path--basePath → config basePath → /api
CORS--cors → config cors.enabled → enabled
Rate--rate-limit → config rateLimit.maxRequests → 100

Loopback by default. neurolink serve listens on 127.0.0.1 unless --host (or config host) names an interface. Its routes run tools, remove MCP servers and read conversation memory on behalf of whoever reaches the port, so a listener the network can reach must be authenticated: with --host 0.0.0.0 (or any other non-loopback address) and no NEUROLINK_SERVER_API_KEY, the command refuses to start and exits 1. To run open on purpose — behind a reverse proxy that authenticates, say — pass --allow-unauthenticated or set NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1; the banner then says in so many words that every route is open.

API key. Set NEUROLINK_SERVER_API_KEY to one key, or several separated by commas for rotation, and every route requires one, sent as Authorization: Bearer <key> or X-API-Key: <key>, except the four probes an orchestrator calls without credentials: <basePath>/health, <basePath>/health/live, <basePath>/health/ready and <basePath>/health/startup. <basePath>/health/detailed (runtime and MCP inventory) and <basePath>/version need the key like any other route. Rejected requests get 401. neurolink server start reads the same variable.

NEUROLINK_SERVER_API_KEY=change-me PORT=8080 neurolink serve --host 0.0.0.0
curl -H "Authorization: Bearer change-me" http://localhost:8080/api/agent/providers

Built-in tools. The server's registry holds none of NeuroLink's built-in tools (file reads and writes under the working directory among them) unless --enable-builtin-tools or NEUROLINK_SERVER_ENABLE_BUILTIN_TOOLS=1 is given, so neither POST <basePath>/tools/execute nor a model behind /agent/execute can reach the host's filesystem by default. MCP tools from your configuration are unaffected.

Without a key, tool and MCP execution answer 403. Even on loopback, a server with no authentication middleware refuses POST <basePath>/tools/execute, POST <basePath>/tools/:name/execute, and the MCP reconnect, remove and execute routes with 403 FORBIDDEN; listing tools and servers stays open. The same --allow-unauthenticated / NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1 lifts it.

Container image. The repository's Dockerfile builds an image that runs neurolink serve --host 0.0.0.0 as a non-root user, configured by environment only. The container must therefore be given NEUROLINK_SERVER_API_KEY (or NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1), or it exits at start:

docker build --target production -t neurolink .
docker run -p 3000:3000 \
-e NEUROLINK_SERVER_API_KEY=change-me \
-e OPENAI_API_KEY=sk-... \
neurolink

Use GET /api/health/live for the liveness probe and GET /api/health/ready for readiness; both stay open when an API key is set. SIGTERM drains and exits 0.

Transcription WebSocket. /v1/audio/transcriptions/stream takes the same key, as Authorization: Bearer <key> or, from a browser, as the subprotocol neurolink-api-key.<key> offered next to neurolink. The key in the URL (?token=) is refused unless NEUROLINK_SERVER_WS_ALLOW_QUERY_TOKEN=1, because a URL is written to access logs and Referer headers. Sockets are capped (100 in all, 16 per address; a further upgrade is answered 503), and mode: "file" on a non-loopback listener needs the key. See /docs/features/transcribe for the protocol.

The CLI configuration provides default values that can be overridden programmatically:

// CLI defaults are used when not specified
const server = await createServer(neurolink, {
framework: "hono", // Overrides CLI default
// port uses CLI default if not specified
});

ServerAdapterConfig​

The main configuration object for server adapters.

type ServerAdapterConfig = {
port?: number;
host?: string;
basePath?: string;
cors?: CORSConfig;
rateLimit?: RateLimitConfig;
bodyParser?: BodyParserConfig;
logging?: LoggingConfig;
shutdown?: ShutdownConfig;
redaction?: RedactionConfig;
timeout?: number;
enableMetrics?: boolean;
enableSwagger?: boolean;
disableBuiltInHealth?: boolean;
};

Core Options​

OptionTypeDefaultDescription
portnumber3000Server port to listen on
hoststring"127.0.0.1"Interface to bind; loopback by default. "0.0.0.0" listens on every interface — register authentication first
basePathstring"/api"Base path prefix for all routes
timeoutnumber30000Per-request timeout, on every adapter: a request past it is answered 504 REQUEST_TIMEOUT and its work aborted
enableMetricsbooleantrueEnable metrics endpoint
enableSwaggerbooleanfalseEnable OpenAPI/Swagger documentation (see below)
disableBuiltInHealthbooleanfalseDisable built-in health routes

OpenAPI/Swagger Documentation (enableSwagger)​

When enableSwagger is set to true, the server exposes interactive API documentation endpoints:

EndpointDescription
GET {basePath}/openapi.jsonOpenAPI 3.1 specification in JSON format
GET {basePath}/openapi.yamlOpenAPI 3.1 specification in YAML format
GET {basePath}/docsInteractive Swagger UI documentation

Example URLs (with default basePath /api):

  • http://localhost:3000/api/openapi.json
  • http://localhost:3000/api/openapi.yaml
  • http://localhost:3000/api/docs

The Swagger UI provides an interactive interface where you can:

  • Browse all available API endpoints
  • View request/response schemas
  • Test API calls directly from the browser
  • Download the OpenAPI specification

Security Consideration: In production environments, consider disabling enableSwagger to prevent exposing internal API structure. Alternatively, protect the documentation endpoints with authentication middleware.

Example: Basic Configuration​

import { createServer } from "@juspay/neurolink";

const server = await createServer(neurolink, {
config: {
port: 8080,
host: "127.0.0.1",
basePath: "/v1/api",
timeout: 60000,
enableSwagger: true,
},
});

CORS Configuration​

type CORSConfig = {
enabled?: boolean;
origins?: string[];
methods?: string[];
headers?: string[];
credentials?: boolean;
maxAge?: number;
};
OptionTypeDefaultDescription
enabledbooleantrueEnable CORS support
originsstring[][]Allowed origins (["*"] = any)
methodsstring[]["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]Allowed HTTP methods
headersstring[]["Content-Type", "Authorization", "X-Request-ID", "X-Session-Id"]Allowed headers (a list you pass replaces the default)
credentialsbooleanfalseAllow credentials
maxAgenumber86400Preflight cache max age in seconds

X-Session-Id is the header createSessionMiddleware() reads. That middleware does nothing on a server that does not carry middleware state (carryMiddlewareState, off by default), except on Hono for a request whose client sent its own X-Request-ID, which shares one context with the handler either way; registerAllRoutes with perUserAuth turns the carry on, or set carryMiddlewareState: true on the server. See the middleware guide.

Security note: With no origins listed, no web page can call the server cross-origin. List the origins you serve pages from, or set ["*"] explicitly to allow any domain (then also set an API key). neurolink serve --cors-origin https://myapp.com does the same from the command line. The wildcard is sent as the literal * on every adapter (never the request's own Origin), and ["*"] together with credentials: true is refused when the server is created: a browser will not send credentials to a wildcard, and reflecting the origin to make it do so would let any page act as the user.

Example: Restrictive CORS​

const server = await createServer(neurolink, {
config: {
cors: {
enabled: true,
origins: ["https://myapp.com", "https://staging.myapp.com"],
methods: ["GET", "POST"],
headers: ["Content-Type", "Authorization", "X-Request-ID"],
credentials: true,
maxAge: 3600,
},
},
});

Rate Limit Configuration​

type RateLimitConfig = {
enabled?: boolean;
windowMs?: number;
maxRequests?: number;
message?: string;
skipPaths?: string[];
keyGenerator?: (ctx: ServerContext) => string;
trustProxy?: boolean;
trustedProxies?: string[];
};
OptionTypeDefaultDescription
enabledbooleantrueEnable rate limiting
windowMsnumber900000 (15 min)Time window in milliseconds
maxRequestsnumber100Maximum requests per window
messagestring"Too many requests..."Error message when limit exceeded
skipPathsstring[][]Paths to exclude, each matching itself and everything under it at a segment boundary (/api/health covers /api/health/live, not /api/healthz)
keyGeneratorfunctionclient addressCustom function to generate the rate-limit key
trustProxybooleanfalseRead the client address from X-Forwarded-For. Off, the header is ignored and the TCP peer is the key, so a client cannot choose its own bucket. Turn on only behind your reverse proxy
trustedProxiesstring[][]Your proxies' addresses: their hops are skipped from the right of X-Forwarded-For, and the key is the last hop no trusted proxy added. Empty takes the rightmost entry

What the key is. Without a keyGenerator, every adapter counts requests per client address: the TCP peer, or, with trustProxy, the client the nearest trusted proxy reports. Hono and Koa used to key on X-Forwarded-For alone, which put every direct client in one shared bucket (100 requests exhausted it for everyone) and let a client escape the limit by rotating the header. A request the runtime cannot attribute to any address is let through, and the adapter says so once on stderr. ctx.remoteAddress carries the peer to a custom keyGenerator; read X-Forwarded-For there only if a proxy you control sets it.

Example: Custom Rate Limiting​

const server = await createServer(neurolink, {
config: {
rateLimit: {
enabled: true,
windowMs: 60000, // 1 minute
maxRequests: 30,
skipPaths: ["/api/health", "/api/ready", "/api/version"],
keyGenerator: (ctx) => {
// Rate limit by API key, falling back to the TCP peer. An empty key
// means "do not count"; never key on a header the client controls.
return ctx.headers["x-api-key"] || ctx.remoteAddress || "";
},
},
},
});

// Behind one reverse proxy at 10.0.0.5 that appends the client to X-Forwarded-For:
const proxied = await createServer(neurolink, {
config: {
rateLimit: { trustProxy: true, trustedProxies: ["10.0.0.5"] },
},
});

Body Parser Configuration​

type BodyParserConfig = {
enabled?: boolean;
maxSize?: string;
jsonLimit?: string;
urlEncoded?: boolean;
};
OptionTypeDefaultDescription
enabledbooleantrueEnable body parsing
maxSizestring"10mb"Maximum body size
jsonLimitstring"10mb"JSON body size limit
rawMaxSizestringmaxSize, else "64mb"Largest raw upload (multipart/form-data, audio/*, application/octet-stream); neurolink serve --max-upload 128mb
urlEncodedbooleantrueEnable URL-encoded body parsing

Example: Large Payload Support​

const server = await createServer(neurolink, {
config: {
bodyParser: {
enabled: true,
maxSize: "50mb",
jsonLimit: "50mb",
urlEncoded: true,
},
},
});

Logging Configuration​

type LoggingConfig = {
enabled?: boolean;
level?: "debug" | "info" | "warn" | "error";
includeBody?: boolean;
includeResponse?: boolean;
};
OptionTypeDefaultDescription
enabledbooleantrueEnable request logging
levelstring"info"Log level
includeBodybooleanfalseInclude request body in logs
includeResponsebooleanfalseInclude response body in logs

Example: Debug Logging​

const server = await createServer(neurolink, {
config: {
logging: {
enabled: true,
level: "debug",
includeBody: true,
includeResponse: true,
},
},
});

Shutdown Configuration​

type ShutdownConfig = {
gracefulShutdownTimeoutMs?: number;
drainTimeoutMs?: number;
forceClose?: boolean;
};
OptionTypeDefaultDescription
gracefulShutdownTimeoutMsnumber30000Maximum time to wait for graceful shutdown (30 sec)
drainTimeoutMsnumber15000Time to drain existing connections (15 sec)
forceClosebooleantrueForce close connections after timeout

Example: Custom Shutdown Timeouts​

const server = await createServer(neurolink, {
config: {
shutdown: {
gracefulShutdownTimeoutMs: 60000, // 60 seconds for long-running requests
drainTimeoutMs: 30000, // 30 seconds to drain connections
forceClose: true, // Force close after timeout
},
},
});

Redaction Configuration​

The redaction system provides automatic sanitization of sensitive data in logs and responses. This feature is opt-in and must be explicitly enabled.

type RedactionConfig = {
enabled?: boolean;
additionalFields?: string[];
preserveFields?: string[];
redactToolArgs?: boolean;
redactToolResults?: boolean;
placeholder?: string;
};
OptionTypeDefaultDescription
enabledbooleanfalseEnable redaction (opt-in)
additionalFieldsstring[][]Extra field names to redact
preserveFieldsstring[][]Fields to exclude from redaction
redactToolArgsbooleantrueRedact tool arguments (when enabled)
redactToolResultsbooleantrueRedact tool results (when enabled)
placeholderstring"[REDACTED]"Replacement text for redacted values

Default Redacted Fields​

When redaction is enabled, the following fields are redacted by default:

  • apiKey
  • token
  • authorization
  • credentials
  • password
  • secret
  • request
  • args
  • result

Example: Custom Redaction​

const server = await createServer(neurolink, {
config: {
redaction: {
enabled: true,
additionalFields: ["ssn", "creditCard", "bankAccount"],
preserveFields: ["request"], // Allow 'request' field to pass through
redactToolArgs: true,
redactToolResults: false, // Keep tool results visible
placeholder: "***",
},
},
});

Example: Minimal Redaction​

const server = await createServer(neurolink, {
config: {
redaction: {
enabled: true,
// Uses all defaults - redacts apiKey, token, password, etc.
},
},
});

Middleware Configuration​

Authentication Middleware​

import { createAuthMiddleware } from "@juspay/neurolink";

const authMiddleware = createAuthMiddleware({
type: "bearer", // 'bearer' | 'api-key' | 'basic' | 'custom'
validate: async (token, ctx) => {
// Return user info or null
const user = await verifyJWT(token);
return user ? { id: user.id, email: user.email, roles: user.roles } : null;
},
headerName: "Authorization", // Optional: custom header name
skipPaths: ["/api/health", "/api/ready"],
errorMessage: "Invalid authentication token",
});

server.registerMiddleware(authMiddleware);

Auth Types​

TypeHeader FormatDescription
bearerAuthorization: Bearer <token>JWT/OAuth token
api-keyX-API-Key: <key>API key authentication
basicAuthorization: Basic <base64>HTTP Basic auth
customCustomUse extractToken function

Rate Limit Middleware​

import {
createRateLimitMiddleware,
createSlidingWindowRateLimitMiddleware,
} from "@juspay/neurolink";

// Fixed window rate limiter
const rateLimiter = createRateLimitMiddleware({
maxRequests: 100,
windowMs: 15 * 60 * 1000,
skipPaths: ["/api/health"],
});

// Sliding window rate limiter (more accurate)
const slidingRateLimiter = createSlidingWindowRateLimitMiddleware({
maxRequests: 100,
windowMs: 15 * 60 * 1000,
subWindows: 10, // Number of sub-windows for smoothing
});

server.registerMiddleware(rateLimiter);

Cache Middleware​

import { createCacheMiddleware, InMemoryCacheStore } from "@juspay/neurolink";

const cacheMiddleware = createCacheMiddleware({
ttlMs: 60 * 1000, // 1 minute cache
maxSize: 1000, // Max cached entries
methods: ["GET"], // Only cache GET requests
excludePaths: ["/api/agent/execute", "/api/agent/stream"],
includeQuery: true, // Include query params in cache key
ttlByPath: {
"/api/tools": 5 * 60 * 1000, // 5 minutes for tools
"/api/version": 60 * 60 * 1000, // 1 hour for version
},
});

server.registerMiddleware(cacheMiddleware);

Cache Response Headers​

The cache middleware adds these headers to responses:

HeaderDescriptionExample
X-CacheCache statusHIT or MISS
X-Cache-AgeSeconds since cached (on HIT)45
Cache-ControlCaching directive (on MISS)max-age=300

Validation Middleware​

import {
createRequestValidationMiddleware,
createFieldValidator,
} from "@juspay/neurolink";

// JSON Schema validation
const validationMiddleware = createRequestValidationMiddleware({
body: {
type: "object",
properties: {
input: { type: "string", minLength: 1 },
provider: { type: "string" },
},
required: ["input"],
},
});

// Field-level validation
const fieldValidator = createFieldValidator({
required: ["name", "email"],
types: { name: "string", email: "string", age: "number" },
validators: {
email: (value) => typeof value === "string" && value.includes("@"),
age: (value) => typeof value === "number" && value >= 0,
},
});

server.registerMiddleware(validationMiddleware);

Role-Based Access Control​

import { createRoleMiddleware } from "@juspay/neurolink";

// Require any of the specified roles
const adminMiddleware = createRoleMiddleware({
requiredRoles: ["admin", "superuser"],
requireAll: false, // Any role matches
errorMessage: "Admin access required",
});

// Require all specified roles
const superAdminMiddleware = createRoleMiddleware({
requiredRoles: ["admin", "superuser"],
requireAll: true, // All roles required
});

Framework-Specific Options​

Hono​

import { ServerAdapterFactory } from "@juspay/neurolink";

const server = await ServerAdapterFactory.createHono(neurolink, {
port: 3000,
// Hono uses @hono/node-server under the hood
});

For more details, see the Hono Guide.

Express​

const server = await ServerAdapterFactory.createExpress(neurolink, {
port: 3000,
// Express-specific middleware can be added via getFrameworkInstance()
});

const app = server.getFrameworkInstance();
app.use(customExpressMiddleware);

For more details, see the Express Guide.

Fastify​

const server = await ServerAdapterFactory.createFastify(neurolink, {
port: 3000,
// Fastify plugins can be registered on the instance
});

const fastify = server.getFrameworkInstance();
await fastify.register(customFastifyPlugin);

For more details, see the Fastify Guide.

Koa​

const server = await ServerAdapterFactory.createKoa(neurolink, {
port: 3000,
// Koa middleware can be added via getFrameworkInstance()
});

const app = server.getFrameworkInstance();
app.use(customKoaMiddleware);

For more details, see the Koa Guide.

Complete Configuration Example​

import {
createServer,
createAuthMiddleware,
createRateLimitMiddleware,
createCacheMiddleware,
} from "@juspay/neurolink";
import { NeuroLink } from "@juspay/neurolink";

const neurolink = new NeuroLink({
defaultProvider: "openai",
});

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 8080,
host: "0.0.0.0",
basePath: "/v1",
timeout: 120000,
enableSwagger: true,
cors: {
enabled: true,
origins: ["https://app.example.com"],
credentials: true,
},
rateLimit: {
enabled: true,
maxRequests: 1000,
windowMs: 3600000,
},
bodyParser: {
maxSize: "25mb",
},
logging: {
level: "info",
},
},
});

// Add custom middleware
server.registerMiddleware(
createAuthMiddleware({
type: "bearer",
validate: async (token) => verifyToken(token),
skipPaths: ["/v1/health", "/v1/ready"],
}),
);

server.registerMiddleware(
createCacheMiddleware({
ttlMs: 300000,
methods: ["GET"],
}),
);

// Start server
await server.start();
console.log(`Server running on http://localhost:8080`);

Environment Variables​

The server adapters respect these environment variables:

VariableDescriptionDefault
PORTServer port (neurolink serve)3000
NEUROLINK_SERVER_API_KEYComma-separated API keys; every route except the health probes needs oneunset
NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED1 lets neurolink serve bind a non-loopback host with no key, and serves the tool and MCP execution routes to unauthenticated callers (SDK too)unset
NEUROLINK_SERVER_ENABLE_BUILTIN_TOOLS1 keeps NeuroLink's built-in tools (filesystem among them) in neurolink serve's registryunset
NEUROLINK_SERVER_WS_ALLOW_QUERY_TOKEN1 lets the transcription WebSocket take the API key as ?token=unset
NEUROLINK_SERVER_TIMEOUT_MSPer-request timeout for neurolink serve (the config file's timeout wins)30000
NEUROLINK_STT_EVAL_ENABLED1 mounts the STT evaluation collector (/v1/evals/stt), which is off by defaultunset
NODE_ENVEnvironment modedevelopment
npm_package_versionPackage version (for health endpoint)unknown

The default host is 127.0.0.1 for the SDK's createServer() and for neurolink serve; there is no HOST variable — pass --host or set host in the config.

Configuration Validation​

Invalid configuration will throw errors at initialization:

// This will throw: "Invalid port number"
const server = await createServer(neurolink, {
config: { port: -1 },
});

// This will throw: "Invalid rate limit configuration"
const server = await createServer(neurolink, {
config: { rateLimit: { maxRequests: -100 } },
});

Always validate your configuration in development before deploying to production.

API Endpoints​

The server adapters expose the following endpoints (all prefixed with basePath, default /api):

Health Endpoints​

MethodEndpointDescription
GET/healthBasic health check
GET/readyReadiness probe
GET/liveLiveness probe
GET/versionVersion information

Agent Endpoints​

MethodEndpointDescription
POST/agent/executeExecute agent with input
POST/agent/streamStream agent response

Tool Endpoints​

MethodEndpointDescription
GET/toolsList available tools
GET/tools/searchSearch tools by name or description
POST/tools/executeExecute a tool named in the body (see below)
POST/tools/:name/executeExecute a specific tool (see below)
GET/tools/:nameGet tool metadata

MCP Endpoints​

MethodEndpointDescription
GET/mcp/serversList MCP servers
GET/mcp/servers/:nameOne server's status
POST/mcp/servers/:name/reconnectReconnect a server (see below)
DELETE/mcp/servers/:nameRemove a server (see below)
GET/mcp/servers/:name/toolsOne server's tools
POST/mcp/servers/:name/tools/:toolName/executeExecute one server's tool (below)
GET/mcp/healthMCP subsystem health check

Execution routes refuse an unauthenticated server. The routes marked "see below" act on the host. When the server has no middleware marked authenticates: true (the built-in createAuthMiddleware, createBearerAuthMiddleware and the CLI's API-key middleware mark themselves), they answer 403 FORBIDDEN on every request. registerAllRoutes() wires the check; createAllRoutes() applies it only when given hasAuthentication. Opt out with registerAllRoutes(server, "/api", { allowUnauthenticated: true }) or NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1, or leave the tool routes out altogether with { tools: false }. Set authenticates: true on your own authentication middleware so the routes recognise it.

Error responses​

A failed request answers with a fixed message for its category and the request id, never the exception's text, which stays in the server log under that id:

CodeStatusWhen
VALIDATION_ERROR etc.4xxA message this server composed about the request
PROVIDER_NOT_CONFIGURED503A provider refused the server's credentials or has none
UPSTREAM_RATE_LIMITED503A provider is rate limiting the server
REQUEST_TIMEOUT504The request ran past timeout; its generation was aborted
INTERNAL_ERROR500Anything else

SSE error events on /agent/stream carry the same code, message and requestId.

Memory Endpoints​

MethodEndpointDescription
GET/memory/sessionsList memory sessions
GET/memory/sessions/:idGet session details
DELETE/memory/sessions/:idDelete a session
DELETE/memory/sessionsClear all sessions
GET/memory/healthMemory subsystem health check

OpenAPI Endpoints (when enableSwagger: true)​

MethodEndpointDescription
GET/openapi.jsonOpenAPI 3.1 spec (JSON)
GET/openapi.yamlOpenAPI 3.1 spec (YAML)
GET/docsSwagger UI

Lifecycle Management​

Server adapters implement a comprehensive lifecycle management system that enables graceful startup, connection tracking, and orderly shutdown. Understanding the lifecycle is essential for production deployments.

Lifecycle States​

The server adapter progresses through 9 distinct lifecycle states:

StateDescription
uninitializedInitial state before initialize() is called
initializingFramework and routes are being set up
initializedSetup complete, ready to start
startingServer is binding to port and preparing to listen
runningServer is actively accepting and processing requests
drainingNo new connections accepted, existing ones finishing
stoppingServer is closing after connections drained
stoppedServer has completely shut down
errorAn error occurred during any state transition

State Transition Diagram​

                    ┌─────────────────┐
│ uninitialized │◄──────────────────────────────────┐
└────────┬────────┘ │
│ initialize() │
▼ │
┌─────────────────┐ │
│ initializing │ │
└────────┬────────┘ │
│ success │
▼ │
┌─────────────────┐ │
│ initialized │◄──────────────────────────────────┤
└────────┬────────┘ │
│ start() │
▼ │
┌─────────────────┐ │
│ starting │ │
└────────┬────────┘ │
│ bound to port │
▼ │
┌─────────────────┐ │
│ running │ │
└────────┬────────┘ │
│ stop() │
▼ │
┌─────────────────┐ │
│ draining │──── drain timeout ────┐ │
└────────┬────────┘ │ │
│ connections drained │ │
▼ ▼ │
┌─────────────────┐ forceClose() │
│ stopping │◄──────────────────────┘ │
└────────┬────────┘ │
│ server closed │
▼ │
┌─────────────────┐ │
│ stopped │───────────────────────────────────┘
└─────────────────┘ (can restart)

Any state ─────────► ┌─────────────────┐
(on error) │ error │
└─────────────────┘

Valid State Transitions​

Current StateValid Next StatesTrigger
uninitializedinitializinginitialize() called
initializinginitialized, errorSetup completes or fails
initializedstartingstart() called
startingrunning, errorPort bound or bind fails
runningdrainingstop() called
drainingstoppingConnections drained/timeout
stoppingstopped, errorServer closes
stoppedinitializinginitialize() for restart
error(terminal, requires new instance)N/A

InvalidLifecycleStateError​

Attempting an operation in an invalid state throws InvalidLifecycleStateError:

import { InvalidLifecycleStateError } from "@juspay/neurolink";

try {
await server.start(); // Called when already running
} catch (error) {
if (error instanceof InvalidLifecycleStateError) {
console.log(`Operation: ${error.operation}`);
console.log(`Current state: ${error.currentState}`);
console.log(`Expected states: ${error.expectedStates.join(", ")}`);
}
}
// Output:
// Operation: start
// Current state: running
// Expected states: initialized, stopped

Querying Lifecycle State​

// Get current lifecycle state
const state = server.getLifecycleState();
console.log(`Server state: ${state}`);

// Get full server status including lifecycle
const status = server.getStatus();
console.log({
running: status.running,
lifecycleState: status.lifecycleState,
activeConnections: status.activeConnections,
uptime: status.uptime,
});

Connection Tracking​

Server adapters track active connections to enable graceful shutdown. This is essential for ensuring in-flight requests complete before the server stops.

TrackedConnection Type​

type TrackedConnection = {
/** Unique connection identifier */
id: string;

/** Timestamp when connection was created */
createdAt: number;

/** Underlying socket or connection object */
socket?: unknown;

/** Request ID if associated with a request */
requestId?: string;

/** Whether the connection is currently processing a request */
isActive?: boolean;
};

Connection Tracking Methods​

Framework adapters use these methods internally to track connections:

// Track a new connection (called by adapter implementations)
protected trackConnection(
id: string,
socket?: unknown,
requestId?: string
): void;

// Untrack a connection when completed
protected untrackConnection(id: string): void;

// Get count of active connections (public API)
public getActiveConnectionCount(): number;

Monitoring Active Connections​

// Check active connections before shutdown
const activeCount = server.getActiveConnectionCount();
console.log(`Active connections: ${activeCount}`);

// Include in health check responses
app.get("/health", (req, res) => {
const status = server.getStatus();
res.json({
status: "ok",
connections: status.activeConnections,
lifecycleState: status.lifecycleState,
});
});

Graceful Shutdown​

Graceful shutdown ensures all in-flight requests complete before the server stops, preventing data loss and providing a better user experience.

Shutdown Process​

When stop() is called, the server follows this sequence:

  1. Stop Accepting Connections

    • Server stops accepting new connections
    • New requests receive connection refused
    • State transitions to draining
  2. Drain Existing Connections

    • Wait for in-flight requests to complete
    • Monitor activeConnections count
    • Timeout after drainTimeoutMs
  3. Handle Drain Timeout

    • If connections remain after drainTimeoutMs:
      • If forceClose: true, forcibly close all connections
      • If forceClose: false, throw DrainTimeoutError
  4. Close Server

    • Close the underlying server
    • State transitions to stopping, then stopped
    • Overall timeout enforced by gracefulShutdownTimeoutMs

Shutdown Configuration Options​

OptionTypeDefaultDescription
gracefulShutdownTimeoutMsnumber30000Maximum total shutdown duration
drainTimeoutMsnumber15000Maximum time to wait for connections to complete
forceClosebooleantrueIf true, forcibly closes connections after drainTimeoutMs expires

Shutdown Example​

import { createServer } from "@juspay/neurolink";
import { ShutdownTimeoutError, DrainTimeoutError } from "@juspay/neurolink";

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
shutdown: {
gracefulShutdownTimeoutMs: 30000,
drainTimeoutMs: 15000,
forceClose: true,
},
},
});

await server.initialize();
await server.start();

// Handle shutdown signals
async function shutdown(signal: string): Promise<void> {
console.log(`Received ${signal}, starting graceful shutdown...`);
console.log(`Active connections: ${server.getActiveConnectionCount()}`);

try {
await server.stop();
console.log("Server stopped gracefully");
process.exit(0);
} catch (error) {
if (error instanceof ShutdownTimeoutError) {
console.error(
`Shutdown timed out with ${error.remainingConnections} connections`,
);
} else if (error instanceof DrainTimeoutError) {
console.error(
`Drain timed out with ${error.remainingConnections} connections`,
);
} else {
console.error("Shutdown error:", error);
}
process.exit(1);
}
}

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

Kubernetes Graceful Shutdown​

For Kubernetes deployments, configure appropriate timeouts:

const server = await createServer(neurolink, {
framework: "hono",
config: {
port: 3000,
shutdown: {
// Should be less than Kubernetes terminationGracePeriodSeconds
gracefulShutdownTimeoutMs: 25000,
drainTimeoutMs: 20000,
forceClose: true,
},
},
});

In your Kubernetes deployment:

spec:
terminationGracePeriodSeconds: 30 # Must be > gracefulShutdownTimeoutMs
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # Allow load balancer to drain

Shutdown Errors​

ErrorDescriptionHandling
ShutdownTimeoutErrorOverall shutdown exceeded gracefulShutdownTimeoutMsForce close was attempted if forceClose: true
DrainTimeoutErrorDrain exceeded drainTimeoutMs with forceClose: falseConnections remain open
InvalidLifecycleStateErrorCalled stop() when not in running stateServer was not running

Server Events​

Server adapters emit events at key lifecycle points. Subscribe to these events for monitoring, logging, and custom behaviors.

Available Events​

type ServerAdapterEvents = {
/** Emitted when server initialization completes */
initialized: {
config: ServerAdapterConfig;
routeCount: number;
middlewareCount: number;
};

/** Emitted when server starts listening */
started: {
port: number;
host: string;
timestamp: Date;
};

/** Emitted when server stops */
stopped: {
uptime: number;
timestamp: Date;
};

/** Emitted for each incoming request */
request: {
requestId: string;
method: string;
path: string;
timestamp: Date;
};

/** Emitted for each outgoing response */
response: {
requestId: string;
statusCode: number;
duration: number;
timestamp: Date;
};

/** Emitted when an error occurs */
error: {
requestId?: string;
error: Error;
timestamp: Date;
};
};

Subscribing to Events​

const server = await createServer(neurolink, {
framework: "hono",
config: { port: 3000 },
});

// Lifecycle events
server.on("initialized", (event) => {
console.log(`Server initialized with ${event.routeCount} routes`);
});

server.on("started", (event) => {
console.log(`Server started on ${event.host}:${event.port}`);
});

server.on("stopped", (event) => {
console.log(`Server stopped after ${event.uptime}ms uptime`);
});

// Request/response events for monitoring
server.on("request", (event) => {
console.log(`[${event.requestId}] ${event.method} ${event.path}`);
});

server.on("response", (event) => {
console.log(
`[${event.requestId}] ${event.statusCode} in ${event.duration}ms`,
);
});

// Error tracking
server.on("error", (event) => {
console.error(`[${event.requestId ?? "unknown"}] Error:`, event.error);
});

await server.initialize();
await server.start();

Event-Based Metrics Collection​

import { createServer } from "@juspay/neurolink";

const metrics = {
requests: 0,
responses: 0,
errors: 0,
totalDuration: 0,
};

const server = await createServer(neurolink, {
framework: "hono",
config: { port: 3000 },
});

server.on("request", () => {
metrics.requests++;
});

server.on("response", (event) => {
metrics.responses++;
metrics.totalDuration += event.duration;
});

server.on("error", () => {
metrics.errors++;
});

// Expose metrics endpoint
server.registerRoute({
method: "GET",
path: "/metrics/custom",
handler: async () => ({
requests: metrics.requests,
responses: metrics.responses,
errors: metrics.errors,
avgDuration:
metrics.responses > 0 ? metrics.totalDuration / metrics.responses : 0,
activeConnections: server.getActiveConnectionCount(),
lifecycleState: server.getLifecycleState(),
}),
description: "Custom application metrics",
tags: ["monitoring"],
});

OpenAPI Customization​

NeuroLink includes a powerful OpenAPI 3.1 specification generator that creates comprehensive API documentation from your server routes. This section covers how to customize the generated OpenAPI specification.

OpenAPIGenerator Class​

The OpenAPIGenerator class is the core component for generating OpenAPI specifications.

import { OpenAPIGenerator } from "@juspay/neurolink";

const generator = new OpenAPIGenerator({
// Customize API info
info: {
title: "My Custom API",
version: "2.0.0",
description: "Custom API description",
},
// Server configuration
servers: [
{ url: "https://api.example.com", description: "Production" },
{ url: "https://staging-api.example.com", description: "Staging" },
],
// Base path for all routes
basePath: "/v2",
// Include security schemes in the spec
includeSecurity: true,
// Add custom tags
additionalTags: [
{ name: "custom", description: "Custom endpoints" },
{ name: "analytics", description: "Analytics and reporting" },
],
// Add custom schemas
customSchemas: {
CustomRequest: {
type: "object",
properties: {
customField: { type: "string" },
},
},
},
// Pass routes to document
routes: myRouteDefinitions,
});

// Generate the specification
const spec = generator.generate();

// Export as JSON or YAML
const jsonSpec = generator.toJSON(true); // pretty-printed
const yamlSpec = generator.toYAML();

Constructor Options​

OptionTypeDefaultDescription
infoobject-Override API info (title, version, description)
serversarray-Custom server URLs
basePathstring/apiBase path for all routes
includeSecuritybooleantrueInclude security schemes
additionalTagsarray[]Extra API tags
customSchemasobject{}Custom JSON schemas to add
routesarray[]Route definitions to document

Generator Methods​

// Add routes after initialization
generator.addRoutes(routeArray);
generator.addRoute(singleRoute);

// Generate the OpenAPI spec
const spec = generator.generate();

// Export formats
const json = generator.toJSON(true); // pretty-printed JSON
const yaml = generator.toYAML(); // YAML format

Built-in Schemas​

NeuroLink provides pre-defined JSON schemas for common API types.

Error and Response Schemas​

import { ErrorResponseSchema, TokenUsageSchema } from "@juspay/neurolink";

// ErrorResponseSchema
// - error.code (string): Error code identifier
// - error.message (string): Human-readable error message
// - error.details (object): Additional error details
// - metadata.timestamp (date-time): Error timestamp
// - metadata.requestId (string): Request identifier

// TokenUsageSchema
// - input (integer): Input/prompt tokens
// - output (integer): Output/completion tokens
// - total (integer): Total tokens used
// - cacheCreationTokens (integer): Tokens for cache creation
// - cacheReadTokens (integer): Tokens read from cache
// - reasoning (integer): Tokens used for reasoning
// - cacheSavingsPercent (number): Cache savings percentage

Agent Schemas​

import {
AgentExecuteRequestSchema,
AgentExecuteResponseSchema,
AgentInputSchema,
ProviderInfoSchema,
} from "@juspay/neurolink";

// AgentExecuteRequestSchema
// - input (string | object): Agent input
// - provider (string): AI provider to use
// - model (string): Specific model
// - systemPrompt (string): System prompt
// - temperature (number): Sampling temperature (0-2)
// - maxTokens (integer): Maximum tokens to generate
// - tools (string[]): Tool names to enable
// - stream (boolean): Enable streaming
// - sessionId (string): Session ID for memory
// - userId (string): User ID for context

// AgentExecuteResponseSchema
// - content (string): Generated text content
// - provider (string): Provider used
// - model (string): Model used
// - usage (TokenUsage): Token usage
// - toolCalls (array): Tool calls made
// - finishReason (string): Completion reason

Tool Schemas​

import {
ToolDefinitionSchema,
ToolExecuteRequestSchema,
ToolExecuteResponseSchema,
ToolListResponseSchema,
ToolParameterSchema,
} from "@juspay/neurolink";

// ToolDefinitionSchema
// - name (string): Tool name
// - description (string): Tool description
// - source (string): Tool source (builtin, external, custom)
// - parameters (object): Tool parameters schema

// ToolExecuteRequestSchema
// - name (string): Tool name to execute
// - arguments (object): Tool arguments
// - sessionId (string): Session context
// - userId (string): User context

// ToolExecuteResponseSchema
// - success (boolean): Execution success
// - data: Result data
// - error (string): Error message if failed
// - duration (number): Execution duration in ms

MCP Server Schemas​

import {
MCPServerStatusSchema,
MCPServersListResponseSchema,
MCPServerToolSchema,
} from "@juspay/neurolink";

// MCPServerStatusSchema
// - serverId (string): Server ID
// - name (string): Server name
// - status (string): connected | disconnected | error | connecting
// - toolCount (integer): Number of available tools
// - lastHealthCheck (date-time): Last health check timestamp
// - error (string): Error message if in error state

Health Schemas​

import {
HealthResponseSchema,
ReadyResponseSchema,
MetricsResponseSchema,
} from "@juspay/neurolink";

// HealthResponseSchema
// - status (string): ok | degraded | unhealthy
// - timestamp (date-time): Check timestamp
// - uptime (integer): Server uptime in ms
// - version (string): Server version

// ReadyResponseSchema
// - ready (boolean): Overall readiness
// - timestamp (date-time): Check timestamp
// - services.neurolink (boolean): SDK status
// - services.tools (boolean): Tool registry status
// - services.externalServers (boolean): MCP servers status

Template Functions​

The OpenAPI module provides template functions for creating operations and parameters.

Operation Templates​

import {
createGetOperation,
createPostOperation,
createStreamingPostOperation,
createDeleteOperation,
} from "@juspay/neurolink";

// GET operation
const getOp = createGetOperation(
"List users", // summary
"Get all users in the system", // description
["users"], // tags
"UserListResponse", // response schema reference
[limitParam, offsetParam], // optional parameters
);

// POST operation
const postOp = createPostOperation(
"Create user", // summary
"Create a new user", // description
["users"], // tags
"CreateUserRequest", // request schema reference
"UserResponse", // response schema reference
[authHeader], // optional parameters
);

// Streaming POST operation
const streamOp = createStreamingPostOperation(
"Stream data", // summary
"Stream data via SSE", // description
["streaming"], // tags
"StreamRequest", // request schema reference
);

// DELETE operation
const deleteOp = createDeleteOperation(
"Delete user", // summary
"Delete a user by ID", // description
["users"], // tags
[userIdParam], // parameters
);

Parameter Templates​

import {
createPathParameter,
createQueryParameter,
createHeaderParameter,
CommonParameters,
} from "@juspay/neurolink";

// Path parameter
const userIdParam = createPathParameter(
"userId", // name
"User ID", // description
{ type: "string", format: "uuid" }, // schema (optional)
);

// Query parameter
const searchParam = createQueryParameter(
"q", // name
"Search query", // description
{ type: "string" }, // schema (optional)
false, // required (optional, default: false)
);

// Header parameter
const apiKeyHeader = createHeaderParameter(
"X-API-Key", // name
"API key for authentication", // description
true, // required (optional, default: false)
);

// Pre-defined common parameters
const { sessionId, serverName, toolName } = CommonParameters;
const { limitQuery, offsetQuery, searchQuery } = CommonParameters;
const { requestIdHeader, authorizationHeader } = CommonParameters;

Security Schemes​

NeuroLink provides pre-defined security schemes for common authentication methods.

import {
BearerSecurityScheme,
ApiKeySecurityScheme,
BasicSecurityScheme,
} from "@juspay/neurolink";

// Bearer token (JWT)
// {
// type: "http",
// scheme: "bearer",
// bearerFormat: "JWT",
// description: "JWT Bearer token authentication"
// }

// API Key (header)
// {
// type: "apiKey",
// in: "header",
// name: "X-API-Key",
// description: "API key authentication via header"
// }

// Basic auth
// {
// type: "http",
// scheme: "basic",
// description: "HTTP Basic authentication"
// }

Using Security Schemes​

const generator = new OpenAPIGenerator({
includeSecurity: true, // Enables security schemes
});

const spec = generator.generate();
// spec.components.securitySchemes = {
// bearerAuth: BearerSecurityScheme,
// apiKeyAuth: ApiKeySecurityScheme
// }
// spec.security = [{ bearerAuth: [] }, { apiKeyAuth: [] }]

Custom Schema Registration​

Add custom schemas to extend the built-in types.

const generator = new OpenAPIGenerator({
customSchemas: {
// Simple custom schema
MyCustomType: {
type: "object",
required: ["id", "name"],
properties: {
id: { type: "string", format: "uuid" },
name: { type: "string", minLength: 1 },
metadata: { type: "object", additionalProperties: true },
},
},

// Extended schema referencing built-in types
ExtendedAgentResponse: {
allOf: [
{ $ref: "#/components/schemas/AgentExecuteResponse" },
{
type: "object",
properties: {
customField: { type: "string" },
analytics: { $ref: "#/components/schemas/AnalyticsData" },
},
},
],
},

// Enum schema
Priority: {
type: "string",
enum: ["low", "medium", "high", "critical"],
description: "Priority level",
},
},
});

Complete Customization Example​

import {
OpenAPIGenerator,
createGetOperation,
createPostOperation,
createPathParameter,
createQueryParameter,
BearerSecurityScheme,
} from "@juspay/neurolink";

// Create generator with full customization
const generator = new OpenAPIGenerator({
info: {
title: "Enterprise AI API",
version: "3.0.0",
description: `
Enterprise AI API provides secure access to AI capabilities.

## Features
- Multi-model AI generation
- Real-time streaming
- Tool execution
- Conversation memory

## Rate Limits
- Standard: 1000 req/hour
- Enterprise: Unlimited
`.trim(),
},

servers: [
{ url: "https://api.enterprise.com/v3", description: "Production" },
{ url: "https://api.staging.enterprise.com/v3", description: "Staging" },
{ url: "http://localhost:3000/v3", description: "Local Development" },
],

basePath: "/v3",
includeSecurity: true,

additionalTags: [
{ name: "analytics", description: "Usage analytics and reporting" },
{ name: "admin", description: "Administrative operations" },
{ name: "webhooks", description: "Webhook management" },
],

customSchemas: {
// Custom request types
WebhookConfig: {
type: "object",
required: ["url", "events"],
properties: {
url: { type: "string", format: "uri" },
events: {
type: "array",
items: { type: "string", enum: ["execute", "error", "complete"] },
},
secret: { type: "string", description: "HMAC secret for validation" },
},
},

// Custom response types
AnalyticsReport: {
type: "object",
properties: {
period: { type: "string" },
totalRequests: { type: "integer" },
averageLatency: { type: "number" },
tokenUsage: { $ref: "#/components/schemas/TokenUsage" },
topModels: {
type: "array",
items: {
type: "object",
properties: {
model: { type: "string" },
count: { type: "integer" },
},
},
},
},
},
},
});

// Add custom routes
generator.addRoute({
method: "GET",
path: "/v3/analytics",
description: "Get usage analytics for the specified period",
tags: ["analytics"],
responseSchema: { $ref: "#/components/schemas/AnalyticsReport" },
auth: true,
});

generator.addRoute({
method: "POST",
path: "/v3/webhooks",
description: "Register a new webhook endpoint",
tags: ["webhooks"],
requestSchema: { $ref: "#/components/schemas/WebhookConfig" },
responseSchema: {
type: "object",
properties: {
id: { type: "string" },
status: { type: "string" },
},
},
auth: true,
});

// Generate the specification
const spec = generator.generate();

// Export to file
import { writeFileSync } from "fs";
writeFileSync("openapi.json", generator.toJSON(true));
writeFileSync("openapi.yaml", generator.toYAML());

Factory Functions​

For quick OpenAPI generation without instantiating the class:

import {
createOpenAPIGenerator,
generateOpenAPISpec,
generateOpenAPIFromConfig,
} from "@juspay/neurolink";

// Create generator with config
const generator = createOpenAPIGenerator({
basePath: "/api",
includeSecurity: true,
});

// Generate spec directly from routes
const spec = generateOpenAPISpec(routes, {
info: { title: "My API", version: "1.0.0" },
});

// Generate from server adapter configuration
const spec = generateOpenAPIFromConfig(serverConfig, routes);
// Automatically uses host/port from serverConfig

All Available Schemas​

The OpenAPISchemas registry provides access to all built-in schemas:

import { OpenAPISchemas } from "@juspay/neurolink";

// Common
OpenAPISchemas.ErrorResponse;
OpenAPISchemas.TokenUsage;

// Agent
OpenAPISchemas.AgentInput;
OpenAPISchemas.AgentExecuteRequest;
OpenAPISchemas.AgentExecuteResponse;
OpenAPISchemas.ToolCall;
OpenAPISchemas.ProviderInfo;

// Tools
OpenAPISchemas.ToolParameter;
OpenAPISchemas.ToolDefinition;
OpenAPISchemas.ToolListResponse;
OpenAPISchemas.ToolExecuteRequest;
OpenAPISchemas.ToolExecuteResponse;

// MCP
OpenAPISchemas.MCPServerTool;
OpenAPISchemas.MCPServerStatus;
OpenAPISchemas.MCPServersListResponse;

// Memory
OpenAPISchemas.ConversationMessage;
OpenAPISchemas.Session;
OpenAPISchemas.SessionsListResponse;

// Health
OpenAPISchemas.HealthResponse;
OpenAPISchemas.ReadyResponse;
OpenAPISchemas.MetricsResponse;