npm package: @joow/mcp-sdk
Description: MCP SDK for Joow platform
Author: Joow
Latest version: 1.0.0

# @joow/mcp-sdk

[![npm version](https://badge.fury.io/js/@joow%2Fmcp-sdk.svg)](https://badge.fury.io/js/@joow%2Fmcp-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A comprehensive **Model Context Protocol (MCP) SDK** for **NestJS** applications that enables seamless integration of AI tools, resources, and prompts through decorators and dependency injection.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Module Configuration](#module-configuration)
- [Decorators Reference](#decorators-reference)
  - [@McpTool](#mcptool)
  - [@McpResource](#mcpresource)
  - [@McpPrompt](#mcpprompt)
- [Context & Helpers](#context--helpers)
- [Advanced Usage](#advanced-usage)
  - [Custom Validation with Zod](#custom-validation-with-zod)
  - [Progress Reporting](#progress-reporting)
  - [Notifications](#notifications)
  - [Error Handling](#error-handling)
  - [Cross-Tool Communication](#cross-tool-communication)
- [HTTP Transport](#http-transport)
- [Logging & Monitoring](#logging--monitoring)
- [Examples](#examples)
- [API Reference](#api-reference)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
- [License](#license)

## Features

🚀 **Easy NestJS Integration** - Seamlessly integrate MCP into your NestJS applications  
🎯 **Decorator-Based** - Use simple decorators to register tools, resources, and prompts  
🔒 **Type-Safe** - Full TypeScript support with Zod schema validation  
📡 **HTTP Transport** - Built-in HTTP transport with session management  
📊 **Comprehensive Logging** - Detailed logging and monitoring capabilities  
⚡ **Progress Reporting** - Real-time progress updates for long-running operations  
🔔 **Notifications** - Server-to-client notification system  
🛠 **Flexible Architecture** - Modular design with dependency injection  
🎛 **Context Management** - Rich execution context with headers, auth, and custom data  
🔄 **Auto-Discovery** - Automatic scanning and registration of decorated methods

## Installation

```bash
# npm
npm install @joow/mcp-sdk

# yarn
yarn add @joow/mcp-sdk

# pnpm
pnpm add @joow/mcp-sdk
```

### Peer Dependencies

This SDK requires the following peer dependencies:

```bash
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
```

## Quick Start

### 1. Create a Service with MCP Tools

```typescript
// greeting.service.ts
import { Injectable } from "@nestjs/common";
import { McpTool, McpContext, Mcp } from "@joow/mcp-sdk";
import { z } from "zod";

@Injectable()
export class GreetingService {
  @McpTool({
    name: "greet_user",
    title: "Greet User",
    description: "Generate a personalized greeting message",
    inputSchema: z.object({
      name: z.string().describe("Name of the person to greet"),
      language: z
        .enum(["en", "es", "fr"])
        .optional()
        .describe("Language for greeting"),
    }),
  })
  async greetUser(
    input: { name: string; language?: "en" | "es" | "fr" },
    ctx: McpContext
  ) {
    const greetings = {
      en: `Hello, ${input.name}! Welcome to our MCP-powered application.`,
      es: `¡Hola, ${input.name}! Bienvenido a nuestra aplicación impulsada por MCP.`,
      fr: `Bonjour, ${input.name}! Bienvenue dans notre application alimentée par MCP.`,
    };

    const message = greetings[input.language || "en"];

    // Report progress
    await Mcp.report(ctx, 50, 100, "Generating greeting...");

    // Simulate some processing time
    await new Promise((resolve) => setTimeout(resolve, 1000));

    await Mcp.report(ctx, 100, 100, "Greeting complete!");

    return Mcp.text(message);
  }
}
```

### 2. Configure the MCP Module

```typescript
// app.module.ts
import { Module } from "@nestjs/common";
import { McpModule } from "@joow/mcp-sdk";
import { GreetingService } from "./greeting.service";

@Module({
  imports: [
    McpModule.register({
      label: "My MCP Server",
      version: "1.0.0",
      basePath: "mcp",
      include: [GreetingService], // Only scan these services
    }),
  ],
  providers: [GreetingService],
})
export class AppModule {}
```

### 3. Start Your Application

```typescript
// main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  console.log("MCP Server running on http://localhost:3000/mcp");
}
bootstrap();
```

## Core Concepts

### Tools

Tools are functions that perform actions or computations. They receive input parameters and return results.

### Resources

Resources provide access to data or content. They can be static URIs or dynamic templates.

### Prompts

Prompts are reusable templates for AI interactions that can accept arguments.

### Context

Every handler receives a `McpContext` object containing:

- **Registry**: Access to all registered tools/resources/prompts
- **Execution Context**: Request metadata, headers, progress reporting
- **Bag**: Custom data storage for cross-cutting concerns

## Module Configuration

```typescript
McpModule.register({
  label: "My MCP Server", // Server name exposed to clients
  version: "1.0.0", // Semantic version
  basePath: "mcp", // HTTP endpoint path (default: 'mcp')
  include: [ServiceA, ServiceB], // Limit scanning to specific services (recommended)
  enableCorsHint: true, // Enable CORS headers (default: false)
});
```

## Decorators Reference

### @McpTool

Register methods as MCP tools that can perform actions.

```typescript
@McpTool({
  name: 'calculate_sum',
  title: 'Calculate Sum',
  description: 'Add two numbers together',
  inputSchema: z.object({
    a: z.number().describe('First number'),
    b: z.number().describe('Second number'),
  }),
})
async calculateSum(
  input: { a: number; b: number },
  ctx: McpContext,
) {
  const result = input.a + input.b;
  return Mcp.json({ sum: result, inputs: input });
}
```

### @McpResource

Register methods to provide static or dynamic resources.

#### Static Resource

```typescript
@McpResource({
  name: 'user_config',
  uri: 'file:///config/user.json',
  title: 'User Configuration',
  description: 'Current user configuration settings',
  mimeType: 'application/json',
})
async getUserConfig(uri: URL, args: any, ctx: McpContext) {
  const config = await this.configService.getUserConfig();
  return Mcp.resource.json(uri.href, config);
}
```

#### Dynamic Resource with Template

```typescript
@McpResource({
  name: 'user_profile',
  template: 'user://{userId}/profile',
  title: 'User Profile',
  description: 'Get user profile by ID',
  mimeType: 'application/json',
  complete: {
    userId: (value: string) => this.userService.searchUsers(value),
  },
})
async getUserProfile(uri: URL, args: any, ctx: McpContext) {
  const userId = args.userId;
  const profile = await this.userService.getProfile(userId);
  return Mcp.resource.json(uri.href, profile);
}
```

### @McpPrompt

Register methods as reusable prompt templates.

```typescript
@McpPrompt({
  name: 'code_review',
  title: 'Code Review Prompt',
  description: 'Generate a code review prompt',
  argsSchema: {
    language: z.string().describe('Programming language'),
    code: z.string().describe('Code to review'),
    focus: z.enum(['security', 'performance', 'readability']).optional(),
  },
})
codeReviewPrompt(
  args: { language: string; code: string; focus?: string },
  ctx: McpContext,
) {
  const focusText = args.focus ? ` with focus on ${args.focus}` : '';

  return Mcp.prompt.of([
    {
      role: 'system',
      content: {
        type: 'text',
        text: `You are an expert code reviewer. Review the following ${args.language} code${focusText}.`,
      },
    },
    {
      role: 'user',
      content: {
        type: 'text',
        text: `Please review this code:\n\n\`\`\`${args.language}\n${args.code}\n\`\`\``,
      },
    },
  ]);
}
```

## Context & Helpers

### McpContext Object

```typescript
interface McpContext {
  registry: McpRegistry; // Access to all registered components
  exec?: {
    // Execution context
    requestId?: string; // Request ID for tracing
    headers?: IncomingHttpHeaders; // HTTP headers
    notify?: (method: string, params?: any) => Promise<void>; // Send notifications
    progress?: (
      progress: number,
      total?: number,
      message?: string
    ) => Promise<void>; // Report progress
  };
  bag?: Record<string, unknown>; // Custom data storage
}
```

### Response Helpers

The `Mcp` namespace provides utilities for building responses:

```typescript
// Text responses
return Mcp.text("Simple text response");
return Mcp.markdown("# Markdown Content\n\nWith **formatting**");

// JSON responses
return Mcp.json({ key: "value", data: [1, 2, 3] }, true); // pretty-printed

// Key-value responses
return Mcp.kv({ status: "success", count: 42, timestamp: new Date() });

// Empty responses
return Mcp.empty();

// Resource responses
return Mcp.resource.text("file:///data.txt", "File content");
return Mcp.resource.json("config:///app.json", { setting: "value" });
return Mcp.resource.binaryBase64("image:///logo.png", base64Data, "image/png");
```

### Error Handling

```typescript
// Built-in error helpers
return Mcp.errors.notFound("User not found");
return Mcp.errors.invalidArg("Invalid email format");
return Mcp.errors.unauthorized("Access denied");
return Mcp.errors.tooManyRequests("Rate limit exceeded", 5000);
return Mcp.errors.fromException(error);

// Custom errors
return Mcp.error("CUSTOM_ERROR", "Something went wrong", {
  code: "E001",
  retryable: true,
});
```

## Advanced Usage

### Custom Validation with Zod

```typescript
const UserSchema = z.object({
  email: z.string().email('Invalid email format'),
  age: z.number().min(18, 'Must be at least 18').max(120, 'Invalid age'),
  preferences: z.object({
    theme: z.enum(['light', 'dark']),
    notifications: z.boolean(),
  }).optional(),
});

@McpTool({
  name: 'create_user',
  title: 'Create User',
  description: 'Create a new user account',
  inputSchema: UserSchema,
})
async createUser(input: z.infer<typeof UserSchema>, ctx: McpContext) {
  // Input is automatically validated
  const user = await this.userService.create(input);
  return Mcp.json(user);
}
```

### Progress Reporting

```typescript
@McpTool({
  name: 'process_large_dataset',
  title: 'Process Large Dataset',
  description: 'Process a large dataset with progress updates',
  inputSchema: z.object({
    datasetId: z.string(),
  }),
})
async processDataset(input: { datasetId: string }, ctx: McpContext) {
  const dataset = await this.dataService.getDataset(input.datasetId);
  const total = dataset.length;

  for (let i = 0; i < total; i++) {
    // Process item
    await this.processItem(dataset[i]);

    // Report progress
    const progress = ((i + 1) / total) * 100;
    await Mcp.report(ctx, progress, 100, `Processed ${i + 1}/${total} items`);
  }

  return Mcp.text(`Successfully processed ${total} items`);
}
```

### Notifications

```typescript
@McpTool({
  name: 'start_background_job',
  title: 'Start Background Job',
  description: 'Start a background job with notifications',
  inputSchema: z.object({
    jobType: z.string(),
  }),
})
async startBackgroundJob(input: { jobType: string }, ctx: McpContext) {
  const jobId = await this.jobService.startJob(input.jobType);

  // Notify client about job start
  await Mcp.notify(ctx, 'job.started', { jobId, type: input.jobType });

  // Set up job completion notification (in real app, this would be async)
  setTimeout(async () => {
    await Mcp.notify(ctx, 'job.completed', { jobId, result: 'success' });
  }, 5000);

  return Mcp.json({ jobId, status: 'started' });
}
```

### Cross-Tool Communication

```typescript
@Injectable()
export class DataProcessingService {
  @McpTool({
    name: "analyze_data",
    title: "Analyze Data",
    description: "Analyze data using other tools",
    inputSchema: z.object({
      data: z.array(z.number()),
    }),
  })
  async analyzeData(input: { data: number[] }, ctx: McpContext) {
    // Call another tool using the registry
    const statsResult = await ctx.registry.callTool("calculate_statistics", {
      numbers: input.data,
    });

    const outlierResult = await ctx.registry.callTool("detect_outliers", {
      data: input.data,
      threshold: 2.0,
    });

    return Mcp.json({
      statistics: statsResult,
      outliers: outlierResult,
      totalPoints: input.data.length,
    });
  }

  @McpTool({
    name: "calculate_statistics",
    title: "Calculate Statistics",
    description: "Calculate basic statistics for numbers",
    inputSchema: z.object({
      numbers: z.array(z.number()),
    }),
  })
  async calculateStatistics(input: { numbers: number[] }, ctx: McpContext) {
    const sum = input.numbers.reduce((a, b) => a + b, 0);
    const mean = sum / input.numbers.length;
    const variance =
      input.numbers.reduce((acc, num) => acc + Math.pow(num - mean, 2), 0) /
      input.numbers.length;

    return {
      mean,
      variance,
      standardDeviation: Math.sqrt(variance),
      min: Math.min(...input.numbers),
      max: Math.max(...input.numbers),
    };
  }
}
```

## HTTP Transport

The SDK provides built-in HTTP transport with session management:

### Endpoints

- `POST /mcp` - Main MCP communication endpoint
- `GET /mcp` - Session-based requests (requires `mcp-session-id` header)
- `DELETE /mcp` - Session cleanup (requires `mcp-session-id` header)

### Session Management

Sessions are automatically created and managed:

```typescript
// Client receives session ID in response header: Mcp-Session-Id
// Use this ID in subsequent requests as: mcp-session-id
```

### Custom HTTP Configuration

```typescript
// Custom base path
McpModule.register({
  label: "My Server",
  basePath: "api/v1/mcp", // Available at /api/v1/mcp
});
```

## Logging & Monitoring

### Built-in Logging

The SDK provides comprehensive logging out of the box:

```typescript
// All operations are automatically logged with:
// - Request/response timing
// - Input/output data (with sensitive data redacted)
// - Error details and stack traces
// - Session lifecycle events
```

### Custom Logging Integration

```typescript
import { McpLoggerService } from "@joow/mcp-sdk";

@Injectable()
export class MyService {
  constructor(private readonly mcpLogger: McpLoggerService) {}

  @McpTool({
    name: "custom_operation",
    title: "Custom Operation",
    description: "Operation with custom logging",
    inputSchema: z.object({ id: z.string() }),
  })
  async customOperation(input: { id: string }, ctx: McpContext) {
    const span = this.mcpLogger.begin(
      "custom",
      "my-operation",
      input,
      ctx.exec
    );

    try {
      const result = await this.performOperation(input.id);
      span.ok({ resultSize: result.length });
      return Mcp.json(result);
    } catch (error) {
      span.err(error, { operationId: input.id });
      return Mcp.errors.fromException(error);
    }
  }
}
```

## Examples

### Complete File Management Service

```typescript
import { Injectable } from "@nestjs/common";
import { McpTool, McpResource, McpContext, Mcp } from "@joow/mcp-sdk";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

@Injectable()
export class FileManagerService {
  private readonly baseDir = "./data";

  @McpTool({
    name: "create_file",
    title: "Create File",
    description: "Create a new file with content",
    inputSchema: z.object({
      filename: z.string().min(1, "Filename required"),
      content: z.string(),
      overwrite: z.boolean().default(false),
    }),
  })
  async createFile(
    input: { filename: string; content: string; overwrite?: boolean },
    ctx: McpContext
  ) {
    const filePath = path.join(this.baseDir, input.filename);

    try {
      // Check if file exists
      if (!input.overwrite) {
        try {
          await fs.access(filePath);
          return Mcp.errors.conflict(
            "File already exists. Use overwrite=true to replace."
          );
        } catch {
          // File doesn't exist, proceed
        }
      }

      await Mcp.report(ctx, 25, 100, "Creating directory if needed...");
      await fs.mkdir(path.dirname(filePath), { recursive: true });

      await Mcp.report(ctx, 75, 100, "Writing file...");
      await fs.writeFile(filePath, input.content, "utf8");

      await Mcp.report(ctx, 100, 100, "File created successfully");

      // Notify about file creation
      await Mcp.notify(ctx, "file.created", {
        filename: input.filename,
        size: input.content.length,
      });

      return Mcp.json({
        filename: input.filename,
        size: input.content.length,
        path: filePath,
        created: new Date().toISOString(),
      });
    } catch (error) {
      return Mcp.errors.fromException(error);
    }
  }

  @McpResource({
    name: "file_content",
    template: "file://{filename}",
    title: "File Content",
    description: "Get content of a file",
    mimeType: "text/plain",
  })
  async getFileContent(uri: URL, args: any, ctx: McpContext) {
    const filename = args.filename;
    const filePath = path.join(this.baseDir, filename);

    try {
      const content = await fs.readFile(filePath, "utf8");
      return Mcp.resource.text(uri.href, content);
    } catch (error) {
      if (error.code === "ENOENT") {
        throw new Error(`File not found: ${filename}`);
      }
      throw error;
    }
  }

  @McpTool({
    name: "list_files",
    title: "List Files",
    description: "List all files in the data directory",
    inputSchema: z.object({
      pattern: z
        .string()
        .optional()
        .describe("Optional glob pattern to filter files"),
    }),
  })
  async listFiles(input: { pattern?: string }, ctx: McpContext) {
    try {
      const files = await fs.readdir(this.baseDir, { withFileTypes: true });
      const fileList = [];

      for (const file of files) {
        if (file.isFile()) {
          const filePath = path.join(this.baseDir, file.name);
          const stats = await fs.stat(filePath);

          if (!input.pattern || file.name.includes(input.pattern)) {
            fileList.push({
              name: file.name,
              size: stats.size,
              modified: stats.mtime.toISOString(),
              uri: `file://${file.name}`,
            });
          }
        }
      }

      return Mcp.json({
        files: fileList,
        total: fileList.length,
        directory: this.baseDir,
      });
    } catch (error) {
      return Mcp.errors.fromException(error);
    }
  }
}
```

## API Reference

### Core Types

```typescript
// Tool definition
interface ToolMeta {
  name: string;
  title?: string;
  description?: string;
  inputSchema?: ZodSchema;
}

// Resource definition
interface ResourceMeta {
  name: string;
  title?: string;
  description?: string;
  uri?: string; // Static resource
  template?: string; // Dynamic resource template
  mimeType?: string;
  complete?: Record<string, (value: string, ctx?: any) => string[]>;
}

// Prompt definition
interface PromptMeta {
  name: string;
  title?: string;
  description?: string;
  argsSchema?: Record<string, ZodSchema>;
}
```

### Registry Methods

```typescript
interface McpRegistry {
  callTool(
    name: string,
    input: any,
    bag?: Record<string, unknown>
  ): Promise<any>;
  getTool(name: string): ToolDef | undefined;
  listTools(): ToolDef[];
  listResources(): ResourceDef[];
  listPrompts(): PromptDef[];
}
```

## Best Practices

### 1. Service Organization

```typescript
// ✅ Good: Organize related functionality in focused services
@Injectable()
export class UserManagementService {
  @McpTool({ name: 'create_user', ... })
  async createUser(input, ctx) { ... }

  @McpTool({ name: 'update_user', ... })
  async updateUser(input, ctx) { ... }

  @McpResource({ name: 'user_profile', ... })
  async getUserProfile(uri, args, ctx) { ... }
}

// ❌ Avoid: Mixing unrelated functionality
@Injectable()
export class MegaService {
  @McpTool({ name: 'create_user', ... })
  async createUser(input, ctx) { ... }

  @McpTool({ name: 'send_email', ... }) // Unrelated to user management
  async sendEmail(input, ctx) { ... }
}
```

### 2. Input Validation

```typescript
// ✅ Good: Comprehensive validation with descriptive messages
const CreateUserSchema = z.object({
  email: z
    .string()
    .email("Must be a valid email address")
    .min(5, "Email must be at least 5 characters")
    .max(254, "Email too long"),
  name: z
    .string()
    .min(2, "Name must be at least 2 characters")
    .max(100, "Name too long")
    .regex(/^[a-zA-Z\s]+$/, "Name can only contain letters and spaces"),
  age: z
    .number()
    .int("Age must be a whole number")
    .min(13, "Must be at least 13 years old")
    .max(120, "Invalid age"),
});

// ❌ Avoid: Weak or missing validation
const WeakSchema = z.object({
  email: z.string(), // No email validation
  name: z.string(), // No length limits
  age: z.number(), // No range validation
});
```

### 3. Error Handling

```typescript
// ✅ Good: Specific, actionable error messages
@McpTool({ name: 'get_user', ... })
async getUser(input: { id: string }, ctx: McpContext) {
  try {
    const user = await this.userService.findById(input.id);
    if (!user) {
      return Mcp.errors.notFound(`User with ID ${input.id} does not exist`);
    }
    return Mcp.json(user);
  } catch (error) {
    if (error instanceof DatabaseConnectionError) {
      return Mcp.errors.transient('Database temporarily unavailable', 30000);
    }
    return Mcp.errors.fromException(error);
  }
}

// ❌ Avoid: Generic error handling
@McpTool({ name: 'get_user', ... })
async getUser(input: { id: string }, ctx: McpContext) {
  const user = await this.userService.findById(input.id); // No error handling
  return Mcp.json(user); // Will throw if user is null
}
```

### 4. Progress Reporting

```typescript
// ✅ Good: Meaningful progress updates
@McpTool({ name: 'import_data', ... })
async importData(input: { file: string }, ctx: McpContext) {
  const data = await this.parseFile(input.file);
  await Mcp.report(ctx, 20, 100, 'File parsed successfully');

  const batches = this.chunkArray(data, 100);
  for (let i = 0; i < batches.length; i++) {
    await this.processBatch(batches[i]);
    const progress = 20 + ((i + 1) / batches.length) * 80;
    await Mcp.report(ctx, progress, 100, `Processed batch ${i + 1}/${batches.length}`);
  }

  return Mcp.json({ imported: data.length, batches: batches.length });
}

// ❌ Avoid: Misleading or missing progress
@McpTool({ name: 'import_data', ... })
async importData(input: { file: string }, ctx: McpContext) {
  await Mcp.report(ctx, 50, 100, 'Processing...'); // Too vague
  const result = await this.processEverything(input.file); // No intermediate updates
  await Mcp.report(ctx, 100, 100, 'Done'); // Not informative
  return Mcp.json(result);
}
```

### 5. Module Configuration

```typescript
// ✅ Good: Explicit service inclusion for better performance
@Module({
  imports: [
    McpModule.register({
      label: "MyApp MCP Server",
      version: "2.1.0",
      basePath: "mcp",
      include: [UserService, FileService, NotificationService], // Only scan these specific services
    }),
  ],
  providers: [UserService, FileService, NotificationService],
})
export class AppModule {}

// ❌ Avoid: Scanning all providers (slower startup)
@Module({
  imports: [
    McpModule.register({
      label: "MyApp",
      // No 'include' - will scan ALL providers
    }),
  ],
  providers: [
    UserService,
    FileService,
    NotificationService,
    DatabaseService,
    CacheService,
  ],
})
export class AppModule {}
```

## Troubleshooting

### Common Issues

#### 1. "Cannot find module '@nestjs/common'"

**Solution:** Install peer dependencies

```bash
npm install @nestjs/common @nestjs/core reflect-metadata rxjs
```

#### 2. "Reflect.defineMetadata is not a function"

**Solution:** Import reflect-metadata in your main.ts

```typescript
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
// ... rest of your code
```

#### 3. "No tools/resources/prompts found"

**Solution:** Ensure you're using the `include` option in module configuration

```typescript
McpModule.register({
  include: [YourService], // Add your services here
});
```

#### 4. "Session not found" errors

**Solution:** Ensure client sends `mcp-session-id` header for GET/DELETE requests

```typescript
// First POST request creates session and returns session ID in header
// Use that ID in subsequent requests
headers: {
  'mcp-session-id': 'received-session-id-from-first-request'
}
```

### Debug Mode

Enable detailed logging by setting the log level:

```typescript
import { Logger } from "@nestjs/common";

// In your main.ts
const app = await NestFactory.create(AppModule, {
  logger: ["error", "warn", "log", "debug", "verbose"],
});
```

### Performance Tips

1. **Use include option** - Limit scanning to only necessary services
2. **Validate early** - Use Zod schemas to catch errors early
3. **Implement progress reporting** - For operations taking >2 seconds
4. **Use appropriate error types** - Help clients handle errors properly
5. **Monitor session cleanup** - Ensure sessions are properly closed

## Contributing

Contributions are welcome! Please follow these steps:

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Commit your changes: `git commit -m 'Add amazing feature'`
4. Push to the branch: `git push origin feature/amazing-feature`
5. Open a Pull Request

### Development Setup

```bash
# Clone the repository
git clone https://github.com/joow/mcp-sdk.git
cd mcp-sdk

# Install dependencies
npm install

# Build the project
npm run build

# Run tests
npm test

# Watch mode for development
npm run dev
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

---

## Support

- 📖 [Documentation](https://github.com/joow/mcp-sdk#readme)
- 🐛 [Issue Tracker](https://github.com/joow/mcp-sdk/issues)
- 💬 [Discussions](https://github.com/joow/mcp-sdk/discussions)

---

Made with ❤️ for the NestJS and MCP communities.