TypeScript · Static Analysis · Compiler Security
MCP server TypeScript strict mode security
TypeScript's type system can catch security bugs at compile time that would otherwise reach production. A null dereference in an authentication check, an unchecked array index in a path validation routine, or an any-typed variable flowing into a credential comparison — these are all detectable by the TypeScript compiler before the code ships, if the right flags are enabled. This reference covers the six compiler flags with the highest security impact for MCP servers.
The security-relevant tsconfig.json
{
"compilerOptions": {
// The strict umbrella (enables strictNullChecks, strictFunctionTypes,
// strictBindCallApply, strictPropertyInitialization, noImplicitAny,
// noImplicitThis, alwaysStrict)
"strict": true,
// Beyond strict: catches unchecked array/object index access
// (array[i] returns T | undefined, not T)
"noUncheckedIndexedAccess": true,
// Prevents { prop?: string } from accepting { prop: undefined } as valid
"exactOptionalPropertyTypes": true,
// Prevents type narrowing bypass via unchecked return types
"noImplicitReturns": true,
// Prevents accidental any from falling-through switch cases
"noFallthroughCasesInSwitch": true,
// Reports unused parameters that might hide forgotten validation
"noUnusedParameters": true,
// Required for correct module resolution in Node.js ESM
"moduleResolution": "node16",
"module": "node16",
"target": "es2022"
}
}
Flag 1: strict (the umbrella)
The strict flag enables a group of type checks that are individually controllable but almost always wanted together. The most security-relevant subset is strictNullChecks:
// Without strictNullChecks — type system doesn't distinguish null/undefined
const apiKey: string = process.env.ANTHROPIC_API_KEY; // compiles fine
// At runtime: apiKey === undefined if the env var is missing
// crypto.timingSafeEqual(Buffer.from(apiKey), ...) throws TypeError
// With strictNullChecks — compiler catches the missing check
const apiKey: string = process.env.ANTHROPIC_API_KEY;
// Error: Type 'string | undefined' is not assignable to type 'string'
// Forces: const apiKey = process.env.ANTHROPIC_API_KEY ?? '';
// if (!apiKey) throw new Error('Missing ANTHROPIC_API_KEY');
This matters for MCP server authentication: if the API key environment variable is undefined and the check is apiKey === undefined is never performed, the server starts with a null authentication key. Depending on the comparison logic, this can allow any caller to authenticate as a match against undefined.
Flag 2: noUncheckedIndexedAccess
Array indexing in TypeScript without this flag returns T — the type system pretends array[99] always returns a value. With noUncheckedIndexedAccess, array indexing returns T | undefined, forcing the developer to handle the case where the index is out of bounds:
// Without noUncheckedIndexedAccess
const allowedPaths = ['/data/uploads/', '/tmp/scratch/'];
const firstPath: string = allowedPaths[0]; // type: string (fine)
const userPath: string = allowedPaths[userIndex]; // type: string (WRONG — could be undefined)
// if userIndex is 99, userPath === undefined
// userPath.startsWith('/data/') throws TypeError at runtime
// With noUncheckedIndexedAccess
const userPath: string | undefined = allowedPaths[userIndex];
// Compiler forces: if (!userPath) throw new ToolError('INVALID_INPUT', 'Index out of range');
// OR: const userPath = allowedPaths[userIndex] ?? '/data/';
In MCP tool handlers that select from arrays of allowed values (paths, schemas, tools) using a caller-supplied index, unchecked indexing can produce undefined values that bypass string-based validation checks (because undefined.startsWith() throws before the check runs, and the error path may be less guarded than the happy path).
Flag 3: exactOptionalPropertyTypes
TypeScript's optional property syntax (prop?: string) normally means "prop may be absent OR prop may be undefined." With exactOptionalPropertyTypes, it means only "prop may be absent" — explicitly setting prop: undefined is a type error. This matters for MCP server configuration schemas:
// Without exactOptionalPropertyTypes
interface McpServerConfig {
authToken?: string;
maxRetries?: number;
}
// This is valid — explicitly sets authToken to undefined
const config: McpServerConfig = { authToken: undefined };
// If the auth check is: if (config.authToken) { verify(config.authToken) }
// Then config with authToken: undefined is treated the same as config without authToken
// This is usually fine, but can cause surprises in complex conditional auth logic
// With exactOptionalPropertyTypes
const config: McpServerConfig = { authToken: undefined };
// Error: Type '{ authToken: undefined; }' is not assignable to 'McpServerConfig'
// Forces either { authToken: 'actual-value' } or {} (omit the property)
Flag 4: noImplicitAny
noImplicitAny (included in strict) prevents TypeScript from silently inferring any when it cannot determine a type. any types are security-relevant because they bypass all type checking — a variable typed as any can be passed to security-sensitive functions without the compiler verifying the shape:
// noImplicitAny catches this — the parameter type is implicitly any
function authenticate(token) { // Error: Parameter 'token' implicitly has an 'any' type
return db.query('SELECT * FROM sessions WHERE token = ?', [token]);
}
// Forces explicit typing, which the developer must think through:
function authenticate(token: string): Promise<Session | null> {
if (typeof token !== 'string' || token.length === 0) return Promise.resolve(null);
return db.query('SELECT * FROM sessions WHERE token = ?', [token]);
}
// Any that flows in from JSON parsing must be explicitly typed via Zod:
const parsed: unknown = JSON.parse(body); // unknown, not any
const validated = SessionTokenSchema.parse(parsed); // Zod validates the shape
Flag 5: noImplicitReturns
A function that sometimes returns a value and sometimes falls off the end returns undefined silently. In authentication and authorization functions, a missing return can produce a bypass:
// Without noImplicitReturns — this compiles fine
function checkPermission(role: string, tool: string): boolean {
if (role === 'admin') return true;
if (role === 'reader' && tool.startsWith('read_')) return true;
// Forgot the final return false — function returns undefined
// undefined is falsy, so callers checking if (!checkPermission(...)) will block correctly
// BUT callers checking if (checkPermission(...) === false) will NOT block
}
// With noImplicitReturns
// Error: Not all code paths return a value
// Forces: return false; as the final case
Flag 6: noUnusedParameters
Unused parameters in tool handlers sometimes indicate forgotten validation that was supposed to be there. In an MCP context, an unused callerId parameter that was meant to be used for per-caller rate limiting is a bug that noUnusedParameters will catch:
// Without noUnusedParameters — compiles, but the rate limiter is never called
async function handleReadFile(filePath: string, callerId: string) {
// callerId parameter intended for rate limiting — forgotten
return fs.readFile(filePath, 'utf8');
}
// With noUnusedParameters
// Error: 'callerId' is declared but its value is never read
// Forces either using it or prefixing with _ to mark intentional non-use:
async function handleReadFile(filePath: string, _callerId: string) { ... }
Recommended tsconfig.json for MCP servers
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"moduleResolution": "node16",
"module": "node16",
"target": "es2022",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": false // false = check type definitions — catches type-unsafe library usage
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Start with "strict": true — it covers the majority of security-relevant checks. Add noUncheckedIndexedAccess and exactOptionalPropertyTypes when you introduce Zod validation schemas (they work well together — Zod schemas with strict TypeScript types produce exhaustive validation). Run tsc --noEmit as a required CI step before any deployment.
SkillAudit findings for TypeScript configuration
any-typed input without runtime validation. Neither compile-time nor runtime type enforcement present. Grade impact: −12 (combined with missing Zod validation).
strict: false in tsconfig. Type checking is weakened — null dereferences, implicit any, and unchecked return paths are not caught at compile time. Grade impact: −5 on Documentation/Maintenance axis.
as Type) used in authentication or authorization code paths without runtime validation. Type assertion bypasses the compiler's type checking — if the assertion is wrong at runtime, the type system provided false safety. Grade impact: −5 on Security axis when assertions are in auth code.
Related: Security testing with Vitest · Input validation patterns · Building a SkillAudit-ready MCP server