agent-rules-kit
Version:
Bootstrap of **Cursor** rules (`.mdc`) and mirror documentation (`.md`) for AI agent-guided projects.
722 lines (608 loc) • 15.4 kB
Markdown
---
globs: <root>/**/*.ts,<root>/**/*.js,<root>/package.json,<root>/tsconfig.json
alwaysApply: false
---
# MCP TypeScript SDK Implementation Guide
## TypeScript SDK Features
The TypeScript SDK provides full type safety and modern JavaScript patterns for building MCP servers and clients with excellent developer experience.
### Installation and Setup
```json
// package.json
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"test": "vitest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"tsx": "^4.0.0",
"vitest": "^1.0.0"
}
}
```
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
### Basic Server Implementation
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// Define types for better type safety
interface EchoToolArgs {
message: string;
}
interface QueryToolArgs {
query: string;
limit?: number;
}
// Create server instance
const server = new Server(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{
capabilities: {
resources: { subscribe: true, listChanged: true },
tools: { listChanged: true },
},
}
);
// Resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'file://example.txt',
name: 'Example File',
description: 'An example text file',
mimeType: 'text/plain',
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'file://example.txt') {
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: 'This is example content from TypeScript MCP server',
},
],
};
}
throw new Error(`Resource not found: ${uri}`);
});
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'echo',
description: 'Echo back the input message',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Message to echo back',
},
},
required: ['message'],
},
},
{
name: 'query',
description: 'Execute a database query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'SQL query to execute',
},
limit: {
type: 'number',
description: 'Maximum number of results',
default: 100,
},
},
required: ['query'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'echo': {
const { message } = args as EchoToolArgs;
return {
content: [
{
type: 'text',
text: `Echo: ${message}`,
},
],
};
}
case 'query': {
const { query, limit = 100 } = args as QueryToolArgs;
// Simulate database query
const results = await simulateDbQuery(query, limit);
return {
content: [
{
type: 'text',
text: `Query executed: ${query}\nResults: ${JSON.stringify(
results,
null,
2
)}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Utility functions
async function simulateDbQuery(query: string, limit: number) {
// Simulate async database operation
await new Promise((resolve) => setTimeout(resolve, 100));
return {
query,
rowCount: Math.floor(Math.random() * limit),
executionTime: Math.random() * 100,
};
}
// Main function
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server running on stdio');
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
}
```
### Advanced Server Features
#### Type-Safe Resource Management
```typescript
// Define resource types
interface FileResource {
type: 'file';
path: string;
content?: string;
}
interface DatabaseResource {
type: 'database';
table: string;
schema?: object;
}
type ResourceData = FileResource | DatabaseResource;
class TypedResourceManager {
private resources = new Map<string, ResourceData>();
registerResource(uri: string, data: ResourceData): void {
this.resources.set(uri, data);
}
async getResource(uri: string): Promise<ResourceData | undefined> {
return this.resources.get(uri);
}
async readFileResource(resource: FileResource): Promise<string> {
if (resource.content) {
return resource.content;
}
// Read from filesystem
const fs = await import('fs/promises');
return await fs.readFile(resource.path, 'utf-8');
}
async queryDatabaseResource(
resource: DatabaseResource,
query: string
): Promise<any[]> {
// Execute database query
return [];
}
}
const resourceManager = new TypedResourceManager();
// Register resources
resourceManager.registerResource('file://config.json', {
type: 'file',
path: './config.json',
});
resourceManager.registerResource('db://users', {
type: 'database',
table: 'users',
});
```
#### Middleware Pattern
```typescript
type MiddlewareFunction = (
request: any,
next: () => Promise<any>
) => Promise<any>;
class MiddlewareManager {
private middlewares: MiddlewareFunction[] = [];
use(middleware: MiddlewareFunction): void {
this.middlewares.push(middleware);
}
async execute(request: any, handler: () => Promise<any>): Promise<any> {
let index = 0;
const next = async (): Promise<any> => {
if (index >= this.middlewares.length) {
return await handler();
}
const middleware = this.middlewares[index++];
return await middleware(request, next);
};
return await next();
}
}
// Create middleware
const authMiddleware: MiddlewareFunction = async (request, next) => {
// Check authentication
const authHeader = request.headers?.authorization;
if (!authHeader) {
throw new Error('Authentication required');
}
return await next();
};
const loggingMiddleware: MiddlewareFunction = async (request, next) => {
const start = Date.now();
console.log(
`[${new Date().toISOString()}] ${request.method} request started`
);
try {
const result = await next();
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${
request.method
} completed in ${duration}ms`
);
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(
`[${new Date().toISOString()}] ${
request.method
} failed in ${duration}ms:`,
error
);
throw error;
}
};
// Use middleware
const middlewareManager = new MiddlewareManager();
middlewareManager.use(loggingMiddleware);
middlewareManager.use(authMiddleware);
```
### Client Implementation
```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
class MCPClient {
private client: Client;
private transport: StdioClientTransport;
constructor() {
this.transport = new StdioClientTransport({
command: 'node',
args: ['dist/server.js'],
});
this.client = new Client(
{
name: 'mcp-client',
version: '1.0.0',
},
{
capabilities: {},
}
);
}
async connect(): Promise<void> {
await this.client.connect(this.transport);
}
async disconnect(): Promise<void> {
await this.client.close();
}
async listResources() {
const response = await this.client.request(
{ method: 'resources/list' },
ListResourcesRequestSchema
);
return response.resources;
}
async readResource(uri: string) {
const response = await this.client.request(
{ method: 'resources/read', params: { uri } },
ReadResourceRequestSchema
);
return response.contents;
}
async callTool<T = any>(name: string, arguments_: object): Promise<T> {
const response = await this.client.request(
{
method: 'tools/call',
params: { name, arguments: arguments_ },
},
CallToolRequestSchema
);
return response.content;
}
}
// Usage example
async function clientExample() {
const client = new MCPClient();
try {
await client.connect();
// List and read resources
const resources = await client.listResources();
console.log('Available resources:', resources);
for (const resource of resources) {
const content = await client.readResource(resource.uri);
console.log(`Resource ${resource.name}:`, content);
}
// Call tools
const echoResult = await client.callTool('echo', {
message: 'Hello from TypeScript!',
});
console.log('Echo result:', echoResult);
const queryResult = await client.callTool('query', {
query: 'SELECT * FROM users',
limit: 10,
});
console.log('Query result:', queryResult);
} finally {
await client.disconnect();
}
}
```
### Error Handling
```typescript
class MCPError extends Error {
constructor(public code: string, message: string, public data?: any) {
super(message);
this.name = 'MCPError';
}
}
class ResourceNotFoundError extends MCPError {
constructor(uri: string) {
super('RESOURCE_NOT_FOUND', `Resource not found: ${uri}`);
}
}
class ToolNotFoundError extends MCPError {
constructor(name: string) {
super('TOOL_NOT_FOUND', `Tool not found: ${name}`);
}
}
// Error handling wrapper
function withErrorHandling<T extends (...args: any[]) => any>(handler: T): T {
return (async (...args: Parameters<T>) => {
try {
return await handler(...args);
} catch (error) {
if (error instanceof MCPError) {
throw error;
}
console.error('Unexpected error:', error);
throw new MCPError('INTERNAL_ERROR', 'An internal error occurred', {
originalError: error.message,
});
}
}) as T;
}
// Usage
const safeReadResource = withErrorHandling(async (uri: string) => {
if (!uri.startsWith('file://')) {
throw new ResourceNotFoundError(uri);
}
// Read resource implementation
return 'resource content';
});
```
### Testing with Vitest
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
describe('MCP Server', () => {
let server: Server;
beforeEach(() => {
server = new Server(
{ name: 'test-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
});
afterEach(async () => {
if (server) {
await server.close();
}
});
it('should list resources', async () => {
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'test://resource',
name: 'Test Resource',
mimeType: 'text/plain',
},
],
}));
// Test implementation would depend on testing framework
// This is a conceptual example
});
it('should handle tool calls', async () => {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'test_tool') {
return {
content: [{ type: 'text', text: 'success' }],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Test tool execution
// Implementation would depend on testing setup
});
});
```
### Performance Optimization
#### Connection Pooling
```typescript
class ConnectionPool<T> {
private connections: T[] = [];
private activeConnections = new Set<T>();
private readonly maxSize: number;
private readonly createConnection: () => Promise<T>;
private readonly destroyConnection: (conn: T) => Promise<void>;
constructor(
maxSize: number,
createConnection: () => Promise<T>,
destroyConnection: (conn: T) => Promise<void>
) {
this.maxSize = maxSize;
this.createConnection = createConnection;
this.destroyConnection = destroyConnection;
}
async acquire(): Promise<T> {
// Return existing connection if available
if (this.connections.length > 0) {
const connection = this.connections.pop()!;
this.activeConnections.add(connection);
return connection;
}
// Create new connection if under limit
if (this.activeConnections.size < this.maxSize) {
const connection = await this.createConnection();
this.activeConnections.add(connection);
return connection;
}
// Wait for connection to become available
return new Promise((resolve) => {
const checkForConnection = () => {
if (this.connections.length > 0) {
const connection = this.connections.pop()!;
this.activeConnections.add(connection);
resolve(connection);
} else {
setTimeout(checkForConnection, 10);
}
};
checkForConnection();
});
}
async release(connection: T): Promise<void> {
this.activeConnections.delete(connection);
this.connections.push(connection);
}
async destroy(): Promise<void> {
await Promise.all([
...this.connections.map((conn) => this.destroyConnection(conn)),
...Array.from(this.activeConnections).map((conn) =>
this.destroyConnection(conn)
),
]);
this.connections.length = 0;
this.activeConnections.clear();
}
}
```
#### Caching Layer
```typescript
interface CacheEntry<T> {
value: T;
timestamp: number;
ttl: number;
}
class LRUCache<K, V> {
private cache = new Map<K, CacheEntry<V>>();
private accessOrder = new Map<K, number>();
private accessCounter = 0;
constructor(
private maxSize: number,
private defaultTtl: number = 300000 // 5 minutes
) {}
get(key: K): V | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
// Check TTL
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
this.accessOrder.delete(key);
return undefined;
}
// Update access order
this.accessOrder.set(key, ++this.accessCounter);
return entry.value;
}
set(key: K, value: V, ttl: number = this.defaultTtl): void {
// Evict if at capacity
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
this.evictLRU();
}
this.cache.set(key, {
value,
timestamp: Date.now(),
ttl,
});
this.accessOrder.set(key, ++this.accessCounter);
}
private evictLRU(): void {
let oldestKey: K | undefined;
let oldestAccess = Infinity;
for (const [key, access] of this.accessOrder) {
if (access < oldestAccess) {
oldestAccess = access;
oldestKey = key;
}
}
if (oldestKey !== undefined) {
this.cache.delete(oldestKey);
this.accessOrder.delete(oldestKey);
}
}
}
```
This TypeScript SDK implementation guide provides comprehensive patterns for building type-safe, performant MCP servers and clients with modern JavaScript/TypeScript development practices.