@tscodex/mcp-sdk
TypeScript SDK for creating MCP (Model Context Protocol) servers with seamless Extension integration

π― Overview
@tscodex/mcp-sdk is a production-ready TypeScript SDK for building MCP servers with built-in support for:
- β
Type-safe APIs using TypeBox schemas
- β
Configuration management with Extension integration
- β
Authentication & Authorization with role-based access control
- β
HTTP transport for MCP protocol
- β
Security features (rate limiting, request validation, path sanitization)
- β
Error handling middleware
- β
Graceful shutdown handling
- β
Extension endpoints (health checks, configuration)
- β
AI Client for interacting with AI proxy (OpenAI, OpenRouter, Ollama, etc.)
π¦ Installation
npm install @tscodex/mcp-sdk
Requirements:
- Node.js >= 18.0.0
- TypeScript >= 5.0.0
π Quick Start
Server Name Validation
Server name must:
- Start with a Latin letter (a-z, A-Z)
- Contain only Latin letters, numbers, hyphens (-), and underscores (_)
- Not start with a number
Valid examples: my-server, mcp_images, server123, MyServer
Invalid examples: @tscodex/mcp-images (contains @ and /), 123server (starts with number), my server (contains space)
Minimal Server
import { McpServer, Type } from '@tscodex/mcp-sdk';
const server = new McpServer({
name: 'hello-server',
version: '1.0.0',
description: 'Simple hello world MCP server'
});
const HelloSchema = Type.Object({
name: Type.Optional(Type.String({
description: 'Name to greet',
default: 'World'
}))
});
server.addTool({
name: 'hello-world',
description: 'Greet someone with a personalized message',
schema: HelloSchema,
handler: async (params, context) => {
const name = params.name || 'World';
return {
content: [{
type: 'text',
text: `Hello, ${name}!`
}]
};
}
});
await server.initialize();
await server.start();
console.log(`Server running on port ${server.serverPort}`);
π Core Features
1. Type-Safe Configuration
import { McpServer, Type, Static } from '@tscodex/mcp-sdk';
const ConfigSchema = Type.Object({
apiKey: Type.String({ minLength: 10 }),
timeout: Type.Number({ default: 5000 }),
enabled: Type.Boolean({ default: true })
});
type Config = Static<typeof ConfigSchema>;
const server = new McpServer<Config>({
name: 'api-server',
version: '1.0.0',
description: 'API integration server',
configSchema: ConfigSchema,
loadConfig: async () => {
const extensionConfig = process.env.MCP_CONFIG
? JSON.parse(process.env.MCP_CONFIG)
: {};
return {
timeout: 5000,
enabled: true,
...extensionConfig
};
}
});
server.addTool({
name: 'api-call',
schema: Type.Object({}),
handler: async (params, context) => {
const timeout = context.config.timeout;
const apiKey = context.config.apiKey;
import { filterMcpPublicConfig } from '@tscodex/mcp-sdk';
const publicConfig = filterMcpPublicConfig(context.config);
}
});
2. Authentication & Authorization
import { McpServer, Type, Static } from '@tscodex/mcp-sdk';
enum Roles {
ADMIN = 'admin',
USER = 'user'
}
const SessionSchema = Type.Object({
email: Type.String({ format: 'email' }),
role: Type.Enum(Roles)
});
type Session = Static<typeof SessionSchema>;
const server = new McpServer<Config, Roles, Session>({
name: 'secure-server',
version: '1.0.0',
description: 'Server with role-based access',
auth: {
roles: {
admin: (session, context) => {
const allowedAdmins = context.config.adminEmails || [];
return session.role === Roles.ADMIN &&
allowedAdmins.includes(session.email);
},
user: async (session, context) => {
return session.role === Roles.USER;
}
},
sessionSchema: SessionSchema,
requireSession: true,
loadSession: async (token, context) => {
const response = await fetch(`${context.config.apiUrl}/validate-token`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
return await response.json() as Session;
}
}
});
server.addTool({
name: 'delete-file',
description: 'Delete a file (admin only)',
schema: Type.Object({ path: Type.String() }),
access: [Roles.ADMIN],
handler: async (params, context) => {
console.log(`Admin ${context.session.email} deleted ${params.path}`);
}
});
3. Resources & Prompts
server.addResource({
uri: 'about',
name: 'About',
description: 'Server information',
handler: async (uri, context) => {
return {
contents: [{
uri,
mimeType: 'text/plain',
text: 'Server information...'
}]
};
}
});
server.addPrompt({
name: 'explain-topic',
description: 'Explain a topic',
arguments: Type.Object({
topic: Type.String({ description: 'Topic to explain' })
}),
handler: async (params, context) => {
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: `Explain ${params.topic}`
}
}]
};
}
});
4. Error Handling
import type { ErrorHandler } from '@tscodex/mcp-sdk';
const errorHandler: ErrorHandler = (error, context) => {
if (error instanceof FileNotFoundError) {
return `File not found: ${error.path}. Please check the file path.`;
}
if (error instanceof PermissionError) {
return `Access denied. Please contact administrator.`;
}
return `An error occurred while executing ${context.type} "${context.name}": ${error.message}`;
};
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
description: 'Server with custom error handling',
errorHandler
});
5. Security Features
import { RateLimiter } from '@tscodex/mcp-sdk';
const server = new McpServer({
name: 'secure-server',
version: '1.0.0',
description: 'Server with security features',
securityOptions: {
rateLimit: {
maxRequests: 100,
windowMs: 60000,
message: 'Too many requests'
},
maxRequestBodySize: 10 * 1024 * 1024,
validateRequestSize: true
},
httpOptions: {
requestTimeout: 30000,
keepAliveTimeout: 5000
}
});
6. Logging
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
description: 'Server with custom logger',
logger: {
info: (msg, ...args) => console.log(`[INFO] ${msg}`, ...args),
error: (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args),
warn: (msg, ...args) => console.warn(`[WARN] ${msg}`, ...args),
debug: (msg, ...args) => console.debug(`[DEBUG] ${msg}`, ...args)
}
});
7. AI Client
The SDK provides an AI client for interacting with the AI proxy provided by MCP Manager. This allows your MCP server to use AI capabilities (like OpenAI, OpenRouter, Ollama) without managing API keys directly.
How it works:
When MCP Manager starts your server, it automatically injects:
MCP_AI_PROXY_URL - The URL of the AI proxy endpoint
MCP_AI_PROXY_TOKEN - A unique token for authentication
The proxy acts as a secure intermediary, providing:
- Security - Your server never sees the real API key
- Rate limiting - MCP Manager can limit requests per server
- Model restrictions - Admins can control which models each server can use
- Usage tracking - All requests are logged for monitoring
- Centralized config - One API key configuration for all servers
Basic usage:
import { getAIClient } from '@tscodex/mcp-sdk';
const ai = getAIClient();
if (await ai.isAvailable()) {
const result = await ai.complete('Summarize this text...');
const response = await ai.chat({
messages: [{ role: 'user', content: 'Hello!' }],
temperature: 0.7,
});
}
Using with tools:
import { McpServer, Type, getAIClient } from '@tscodex/mcp-sdk';
const server = new McpServer({ name: 'my-server' });
const ai = getAIClient();
server.addTool({
name: 'summarize',
description: 'Summarize text using AI',
schema: Type.Object({
text: Type.String({ description: 'Text to summarize' }),
}),
handler: async ({ text }) => {
if (!await ai.isAvailable()) {
return {
content: [{ type: 'text', text: 'AI summarization is not available' }],
isError: true,
};
}
const summary = await ai.completeWithSystem(
'You are a helpful assistant that creates concise summaries.',
`Please summarize the following text:\n\n${text}`
);
return {
content: [{ type: 'text', text: summary }],
};
},
});
Error handling:
import { getAIClient, AIClientError } from '@tscodex/mcp-sdk';
const ai = getAIClient();
try {
const result = await ai.complete('Hello');
} catch (error) {
if (error instanceof AIClientError) {
switch (error.code) {
case 'NOT_CONFIGURED':
break;
case 'UNAUTHORIZED':
break;
case 'RATE_LIMITED':
break;
case 'API_ERROR':
break;
case 'TIMEOUT':
break;
case 'NETWORK_ERROR':
break;
}
}
}
Custom configuration:
import { createAIClient } from '@tscodex/mcp-sdk';
const ai = createAIClient({
defaultModel: 'gpt-4',
timeout: 60000,
});
Available methods:
ai.isConfigured(): boolean;
await ai.isAvailable(forceCheck?: boolean): Promise<boolean>;
await ai.getModels(): Promise<ModelsResponse>;
await ai.chat(options: ChatCompletionOptions): Promise<ChatCompletion>;
await ai.complete(prompt: string, options?: ChatCompletionOptions): Promise<string>;
await ai.completeWithSystem(
systemPrompt: string,
userPrompt: string,
options?: ChatCompletionOptions
): Promise<string>;
ai.resetAvailabilityCache(): void;
π API Reference
McpServer<TConfig, TRoles, TSession>
Main server class.
Constructor
interface McpServerOptions<TConfig, TRoles, TSession> {
name: string;
version: string;
description: string;
id?: string;
configSchema?: TSchema;
loadConfig?: () => Promise<TConfig>;
auth?: AuthConfig<TRoles, TSession, TConfig>;
mcpPath?: string;
corsOptions?: CorsOptions;
httpOptions?: ServerHttpOptions;
securityOptions?: ServerSecurityOptions;
handlerOptions?: ServerHandlerOptions;
errorHandler?: ErrorHandler<TConfig, TSession>;
logger?: Logger;
}
Methods
await server.initialize(): Promise<void>;
await server.start(): Promise<void>;
await server.stop(): Promise<void>;
server.addTool<TSchemaType>(config: ToolConfig<TSchemaType, TConfig, TRoles, TSession>): void;
server.addResource(config: ResourceConfig<TConfig, TRoles, TSession>): void;
server.addPrompt<TSchemaType>(config: PromptConfig<TSchemaType, TConfig, TRoles, TSession>): void;
server.getConfig(): TConfig;
server.getProjectRoot(): string | undefined;
server.getSession(): TSession | undefined;
server.getTools(): string[];
server.getResources(): string[];
server.getPrompts(): string[];
server.getMetadata(): ServerMetadata;
server.serverId: string;
server.serverPort: number;
server.serverHost: string;
server.running: boolean;
Events
server.on('initialized', () => {});
server.on('started', (port: number, host: string) => {});
server.on('stopped', () => {});
server.on('error', (error: Error) => {});
server.on('toolRegistered', (name: string) => {});
server.on('toolCalled', (name: string, params: any, result: any) => {});
server.on('toolError', (name: string, params: any, error: Error) => {});
server.on('resourceRegistered', (uri: string) => {});
server.on('resourceRead', (uri: string, result: any) => {});
server.on('resourceError', (uri: string, error: Error) => {});
server.on('promptRegistered', (name: string) => {});
server.on('promptCalled', (name: string, params: any, result: any) => {});
server.on('promptError', (name: string, params: any, error: Error) => {});
server.on('projectRootChanged', (newRoot: string, previousRoot: string) => {});
π Extension Integration
The SDK is designed to work seamlessly with Cursor/VSCode Extensions.
Metadata Mode (--meta flag)
SDK supports metadata mode for Extension integration. When started with --meta or --metadata flag:
- Server outputs only JSON metadata to
stdout (no logs)
- All logs are redirected to
stderr
- Server exits after outputting metadata (doesn't start HTTP server)
- Useful for Extension to discover server capabilities without starting the server
Usage:
node dist/index.js --meta
node dist/index.js --metadata
Programmatic usage:
await server.initialize();
const metadata = server.getMetadata();
console.log(JSON.stringify(metadata, null, 2));
Environment Variables
Extension automatically passes configuration via environment variables:
MCP_PORT - Server port (default: 3848)
MCP_HOST - Server host (default: '0.0.0.0')
MCP_PROJECT_ROOT - Workspace root directory
MCP_CONFIG - Configuration as JSON string
MCP_AUTH_TOKEN - Authentication token/key (for auth-enabled servers)
MCP_PATH - MCP endpoint path (default: '/mcp')
MCP_AI_PROXY_URL - AI proxy endpoint URL (for AI Client)
MCP_AI_PROXY_TOKEN - AI proxy authentication token (for AI Client)
Fallback Support: SDK supports fallback to non-prefixed environment variables for server settings:
MCP_HOST β HOST (if MCP_HOST is not set)
MCP_PORT β PORT (if MCP_PORT is not set)
MCP_PROJECT_ROOT β CURSOR_WORKSPACE β PROJECT_ROOT (if MCP_PROJECT_ROOT is not set)
Priority order: MCP_* env vars β non-prefixed env vars β CLI arguments β defaults
Important: Only environment variables with MCP_ prefix are loaded into application configuration (via loadConfig). This prevents accidental exposure of system environment variables. For example, use MCP_TIMEOUT=5000 instead of TIMEOUT=5000. However, server settings (host, port, project root) support fallback to non-prefixed variables for convenience.
Extension Endpoints
SDK automatically creates endpoints for Extension:
GET /health - Health check with server information
GET /gateway/metadata - Get server metadata (tools, resources, prompts, config schema)
POST /gateway/config/project-root - Update project root
GET /gateway/config/current - Get current configuration (public config only)
POST /gateway/config - Update configuration dynamically (deep merge)
Configuration Management
Important: Extension configuration is updated only by restarting the process with new environment variables. The SDK provides read-only access via getConfig().
Public MCP Configuration Parameters
Handlers receive full configuration (including secrets) in context.config for use in code. SDK provides filterMcpPublicConfig() utility to help handlers return only public MCP parameters (mcp_* keys) in their results.
Example:
import { filterMcpPublicConfig } from '@tscodex/mcp-sdk';
const ConfigSchema = Type.Object({
mcp_timeout: Type.Number({ default: 5000 }),
mcp_api_url: Type.String({ default: 'https://api.example.com' }),
apiKey: Type.String(),
databasePassword: Type.String()
});
handler: async (params, context) => {
const timeout = context.config.timeout;
const apiKey = context.config.apiKey;
const publicConfig = filterMcpPublicConfig(context.config);
return {
content: [{
type: 'text',
text: JSON.stringify(publicConfig)
}]
};
}
This design ensures that:
- Full config (including secrets) is accessible in handlers for use in code
- Public parameters (
mcp_* keys) can be safely returned in results using filterMcpPublicConfig()
- Responsibility for not leaking secrets in MCP responses lies with handlers
π Per-Request Context (Multi-Workspace Support)
When multiple workspaces share a single MCP server process, the SDK supports per-request context via HTTP headers. This allows each request to have its own projectRoot and workspaceId.
How It Works
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Workspace A β β MCP Gateway β β MCP Server β
β /projects/foo ββββββΆβ (Proxy Layer) ββββββΆβ (Shared) β
βββββββββββββββββββ β β β β
β Adds headers: β β Reads headers β
βββββββββββββββββββ β X-MCP-Project- β β via AsyncLocal β
β Workspace B ββββββΆβ Root β β Storage β
β /projects/bar β β X-MCP-Workspaceβ β β
βββββββββββββββββββ β -Id β β β
βββββββββββββββββββ βββββββββββββββββββ
The SDK recognizes these headers for per-request context:
X-MCP-Project-Root | Workspace project root path | Overrides MCP_PROJECT_ROOT env |
X-MCP-Workspace-Id | Workspace identifier (optional) | Informational |
Context Priority
projectRoot is resolved with the following priority:
- Per-request header (
X-MCP-Project-Root) - highest priority
- Server-level environment (
MCP_PROJECT_ROOT)
- undefined if neither is set
Usage in Handlers
server.addTool({
name: 'list-files',
schema: Type.Object({}),
handler: async (params, context) => {
const root = context.projectRoot;
const wsId = context.workspaceId;
if (!root) {
return { content: [{ type: 'text', text: 'No project root configured' }] };
}
const files = await fs.readdir(root);
return {
content: [{ type: 'text', text: files.join('\n') }]
};
}
});
Implementation Details
The SDK uses Node.js AsyncLocalStorage to propagate request context through the async call stack. This allows handlers to access per-request headers even though the official MCP SDK doesn't support custom context in handlers.
import {
getRequestContext,
extractRequestContext,
requestContextStorage
} from '@tscodex/mcp-sdk';
const reqContext = extractRequestContext(httpRequest);
Backward Compatibility
- Servers that don't receive these headers continue to work normally
projectRoot falls back to MCP_PROJECT_ROOT environment variable
workspaceId is undefined when not provided
- Existing plugins don't need any changes
SDK allows servers to declare custom context headers that are passed with each request. This enables workspace-specific data (like project IDs, API keys, etc.) to be sent dynamically without modifying the server configuration.
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
description: 'Server with custom context headers',
contextHeaders: ['project-id', 'api-key', 'region']
});
How It Works
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β MCP Manager β β MCP Gateway β β MCP Server β
β UI shows form β β β β β
β for headers: ββββββΆβ Adds headers: ββββββΆβ Receives in β
β [project-id] β β X-MCP-CTX- β β context. β
β [api-key] β β project-id β β contextHeaders β
β [region] β β X-MCP-CTX- β β β
β β β api-key β β β
β β β X-MCP-CTX- β β β
β β β region β β β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
Using in Handlers
server.addTool({
name: 'get-project-data',
description: 'Fetch data for specific project',
schema: Type.Object({}),
handler: async (params, context) => {
const projectId = context.contextHeaders?.['project-id'];
const apiKey = context.contextHeaders?.['api-key'];
const region = context.contextHeaders?.['region'] || 'us-east-1';
if (!projectId) {
return {
content: [{
type: 'text',
text: 'Error: project-id header is required. Configure it in workspace settings.'
}]
};
}
const data = await fetchProjectData(projectId, apiKey, region);
return {
content: [{ type: 'text', text: JSON.stringify(data) }]
};
}
});
Context headers are passed with the X-MCP-CTX- prefix:
project-id | X-MCP-CTX-project-id | project-id |
api-key | X-MCP-CTX-api-key | api-key |
Region | X-MCP-CTX-Region | region (lowercase) |
Metadata Exposure
Declared context headers are included in server metadata (getMetadata()), allowing MCP Manager to automatically show configuration UI for each workspace:
const metadata = server.getMetadata();
Use Cases
- Multi-tenant applications: Pass tenant ID per workspace
- External service integration: Pass project/account IDs to map workspaces to external services
- Region selection: Allow different regions per workspace
- API key override: Different API keys for different workspaces
π§ Utilities
Security Utilities
import { safePath, isPathSafe, sanitizeFilename, RateLimiter } from '@tscodex/mcp-sdk';
const safe = safePath('/workspace', userPath);
if (isPathSafe(userPath)) {
}
const safe = sanitizeFilename(userFilename);
const limiter = new RateLimiter({
maxRequests: 100,
windowMs: 60000
});
Configuration Utilities
import { validateConfig, updateConfig } from '@tscodex/mcp-sdk';
const config = validateConfig<Config>(data, schema);
const merged = updateConfig(defaultConfig, extensionConfig);
π Documentation
π Examples
Check the examples/ directory for complete examples:
- basic-server.ts - Minimal server setup
- with-config.ts - Configuration management
- with-auth.ts - Authentication & authorization
- with-error-handler.ts - Custom error handling
- file-server.ts - File operations example
- with-ai-client.ts - AI Client integration example
Run examples:
tsx examples/basic-server.ts
tsx examples/with-config.ts
tsx examples/with-auth.ts
ποΈ Architecture
Initialization Flow
1. Extension starts process
β (env vars: MCP_PORT, MCP_HOST, MCP_PROJECT_ROOT, MCP_CONFIG, MCP_AUTH_TOKEN)
2. new McpServer(options)
- Reads port/host/projectRoot from env vars
- Creates HTTP Server
- Creates MCP Server instance
3. server.initialize()
- Loads configuration from MCP_CONFIG
- Calls loadConfig() for local settings
- Merges configurations (Extension takes priority)
- Validates via configSchema
- Loads session if auth is configured
- Filters tools/resources/prompts by access
- Sets up Extension endpoints
- Registers MCP handlers
4. server.addTool/addResource/addPrompt
- Register functionality
5. server.start()
- Starts HTTP Server
- Sets up graceful shutdown handlers
6. Server running
- Handles MCP requests
- Provides Extension endpoints
- Configuration and session available via context
Project Structure
@tscodex/mcp-sdk/
βββ src/
β βββ server.ts # McpServer class
β βββ types.ts # TypeScript types
β βββ config.ts # Configuration management
β βββ transport.ts # HTTP transport
β βββ security.ts # Security utilities
β βββ extension.ts # Extension types
β βββ index.ts # Main exports
βββ examples/ # Example servers
βββ dist/ # Compiled output
π Security Best Practices
- Always validate user input using TypeBox schemas
- Use
safePath() for file operations to prevent path traversal
- Enable rate limiting for production servers
- Sanitize filenames using
sanitizeFilename()
- Validate request sizes to prevent DoS attacks
- Use HTTPS in production (configured at transport level)
- Implement proper authentication for sensitive operations
π License
MIT Β© unbywyd
π Links
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Version: 0.2.0
Status: Production Ready