diff --git a/README.md b/README.md index 7051f1b..3b2f452 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # MCP Typescript SDK by ChatMCP -## How to use +## How to Use -1. install sdk +1. Install SDK ```shell npm i @chatmcp/sdk ``` -2. update MCP Server +2. Configure MCP Server + +### Basic Configuration ```typescript import { RestServerTransport } from "@chatmcp/sdk/server/rest.js"; @@ -24,7 +26,56 @@ async function main() { } ``` -3. request api +### Multi-tenant Support + +```typescript +import { RestServerTransport } from "@chatmcp/sdk/server/rest.js"; + +async function main() { + const port = 9593; + const endpoint = "/api"; + + // Enable multi-tenant support + const transport = new RestServerTransport({ + port, + endpoint, + supportTenantId: true // Enable multi-tenant support + }); + + await server.connect(transport); + await transport.startServer(); + + // Now accessible via /api/{tenantId}, such as /api/tenant1, /api/tenant2 +} +``` + +### API Authentication Support + +```typescript +import { RestServerTransport } from "@chatmcp/sdk/server/rest.js"; + +async function main() { + const port = 9593; + const endpoint = "/secure"; + + // Enable API Key authentication + const transport = new RestServerTransport({ + port, + endpoint, + apiKey: "your-secret-api-key", // Set API key + apiKeyHeaderName: "X-API-Key" // Optional, defaults to "X-API-Key" + // You can also use the standard Authorization header + // apiKeyHeaderName: "Authorization" + }); + + await server.connect(transport); + await transport.startServer(); +} +``` + +3. API Requests + +### Basic Request ```curl curl -X POST http://127.0.0.1:9593/rest \ @@ -43,3 +94,131 @@ curl -X POST http://127.0.0.1:9593/rest \ } }' ``` + +### Multi-tenant Request + +```curl +curl -X POST http://127.0.0.1:9593/api/tenant1 \ +-H "Content-Type: application/json" \ +-d '{ + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + "params": { + "protocolVersion": "1.0", + "capabilities": {}, + "clientInfo": { + "name": "your_client_name", + "version": "your_version" + } + } +}' +``` + +### Request with API Authentication + +```curl +curl -X POST http://127.0.0.1:9593/secure \ +-H "Content-Type: application/json" \ +-H "X-API-Key: your-secret-api-key" \ +-d '{ + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + "params": { + "protocolVersion": "1.0", + "capabilities": {}, + "clientInfo": { + "name": "your_client_name", + "version": "your_version" + } + } +}' +``` + +### Request with API Authentication Using Authorization Header + +```curl +curl -X POST http://127.0.0.1:9593/secure \ +-H "Content-Type: application/json" \ +-H "Authorization: Bearer your-secret-api-key" \ +-d '{ + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + "params": { + "protocolVersion": "1.0", + "capabilities": {}, + "clientInfo": { + "name": "your_client_name", + "version": "your_version" + } + } +}' +``` + +### Accessing Tenant ID in Request Handlers + +When you enable multi-tenant support (`supportTenantId: true`), the tenant ID is added to each request's `params` object as a special parameter `_tenantId`. Here's an example showing how to access the tenant ID in request handlers: + +```typescript +import { RestServerTransport } from "@chatmcp/sdk/server/rest.js"; + +async function main() { + // Create transport with multi-tenant support + const transport = new RestServerTransport({ + port: 9593, + endpoint: "/api", + supportTenantId: true + }); + + await server.connect(transport); + + // Set up request handlers + server.setRequestHandler(ListToolsRequestSchema, async (request) => { + // Get tenant ID + const tenantId = request.params._tenantId; + console.log(`Processing request from tenant ${tenantId}`); + + // Return different tool lists based on tenant ID + return { + tools: tenantId === "admin" ? ADMIN_TOOLS : REGULAR_TOOLS + }; + }); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + // Get tenant ID + const tenantId = request.params._tenantId; + + // Use tenant ID for permission checks or tenant isolation + if (!hasPermission(tenantId, request.params.name)) { + throw new Error(`Tenant ${tenantId} does not have permission to access tool ${request.params.name}`); + } + + // Pass tenant ID to tool execution function for tenant isolation + return await executeToolAndHandleErrors( + request.params.name, + { + ...request.params.arguments || {}, + _tenantId: tenantId // Pass tenant ID to tool execution context + }, + taskManager + ); + }); + + await transport.startServer(); +} + +// Example permission check function +function hasPermission(tenantId: string, toolName: string): boolean { + // Implement your permission check logic + return true; +} +``` + +Using this approach, you can access the tenant ID in request handlers to implement: + +1. Tenant Isolation - Ensure each tenant can only access their own data +2. Tenant-specific Configuration - Provide different tools or features for different tenants +3. Multi-tenant Authentication and Authorization - Combine with API keys for more granular access control +4. Audit Logging - Record access and operations for each tenant diff --git a/package-lock.json b/package-lock.json index 8e2cd60..9dff5cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@chatmcp/sdk", - "version": "1.0.2", + "version": "1.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@chatmcp/sdk", - "version": "1.0.2", + "version": "1.0.6", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", diff --git a/src/server/rest.ts b/src/server/rest.ts index f5fecbb..b02cbbf 100644 --- a/src/server/rest.ts +++ b/src/server/rest.ts @@ -17,6 +17,9 @@ const MAXIMUM_MESSAGE_SIZE = "4mb"; export interface RestServerTransportOptions { endpoint?: string; port?: string | number; + supportTenantId?: boolean; + apiKey?: string; + apiKeyHeaderName?: string; } /** @@ -26,16 +29,34 @@ export interface RestServerTransportOptions { * Usage example: * * ```typescript - * // Create a basic synchronous transport + * // Create basic synchronous transport * const transport = new RestServerTransport({ endpoint: '/rest', port: '9593' }); * await server.connect(transport); * await transport.startServer(); + * + * // Transport with tenant ID support + * const multitenantTransport = new RestServerTransport({ + * endpoint: '/api', + * port: 9593, + * supportTenantId: true + * }); + * + * // Transport with API Key authentication + * const secureTransport = new RestServerTransport({ + * endpoint: '/secure', + * port: 9593, + * apiKey: 'your-secret-api-key', + * apiKeyHeaderName: 'X-API-Key' // Optional, defaults to 'X-API-Key' + * }); * ``` */ export class RestServerTransport implements Transport { private _started: boolean = false; private _endpoint: string; private _port: number; + private _supportTenantId: boolean; + private _apiKey?: string; + private _apiKeyHeaderName: string; private _server: ReturnType | null = null; private _httpServer: ReturnType | null = null; @@ -55,6 +76,9 @@ export class RestServerTransport implements Transport { constructor(options: RestServerTransportOptions = {}) { this._endpoint = options.endpoint || "/rest"; this._port = Number(options.port) || 9593; + this._supportTenantId = options.supportTenantId || false; + this._apiKey = options.apiKey; + this._apiKeyHeaderName = options.apiKeyHeaderName || "X-API-Key"; } /** @@ -66,9 +90,19 @@ export class RestServerTransport implements Transport { } this._server = express(); - this._server.post(this._endpoint, (req, res) => { - this.handleRequest(req, res, req.body); - }); + + if (this._supportTenantId) { + // Add route with tenant ID + this._server.post(`${this._endpoint}/:tenantId`, (req, res) => { + const tenantId = req.params.tenantId; + this.handleRequest(req, res, req.body, tenantId); + }); + } else { + // Keep original route + this._server.post(this._endpoint, (req, res) => { + this.handleRequest(req, res, req.body); + }); + } return new Promise((resolve, reject) => { try { @@ -125,10 +159,11 @@ export class RestServerTransport implements Transport { async handleRequest( req: IncomingMessage, res: ServerResponse, - parsedBody?: unknown + parsedBody?: unknown, + tenantId?: string ): Promise { if (req.method === "POST") { - await this.handlePostRequest(req, res, parsedBody); + await this.handlePostRequest(req, res, parsedBody, tenantId); } else { res.writeHead(405).end( JSON.stringify({ @@ -143,15 +178,44 @@ export class RestServerTransport implements Transport { } } + /** + * Validates API Key from the request header + */ + private validateApiKey(req: IncomingMessage): boolean { + // If no API Key is set, no validation needed + if (!this._apiKey) { + return true; + } + + const providedApiKey = req.headers[this._apiKeyHeaderName.toLowerCase()]; + return providedApiKey === this._apiKey; + } + /** * Handles POST requests containing JSON-RPC messages */ private async handlePostRequest( req: IncomingMessage, res: ServerResponse, - parsedBody?: unknown + parsedBody?: unknown, + tenantId?: string ): Promise { try { + // Validate API Key + if (!this.validateApiKey(req)) { + res.writeHead(401).end( + JSON.stringify({ + jsonrpc: "2.0", + error: { + code: -32001, + message: "Unauthorized: Invalid API Key", + }, + id: null, + }) + ); + return; + } + // validate the Accept header const acceptHeader = req.headers.accept; if ( @@ -223,7 +287,19 @@ export class RestServerTransport implements Transport { // handle each message for (const message of messages) { - this.onmessage?.(message); + if (tenantId && "method" in message) { + // Add tenant ID to each message + const messageWithTenant = { + ...message, + params: { + ...message.params, + _tenantId: tenantId // Add tenant ID as implicit parameter + } + }; + this.onmessage?.(messageWithTenant); + } else { + this.onmessage?.(message); + } } } else if (hasRequests) { // Create a unique identifier for this request batch @@ -245,7 +321,19 @@ export class RestServerTransport implements Transport { // Process all messages for (const message of messages) { - this.onmessage?.(message); + if (tenantId && "method" in message) { + // Add tenant ID to each message + const messageWithTenant = { + ...message, + params: { + ...message.params, + _tenantId: tenantId // Add tenant ID as implicit parameter + } + }; + this.onmessage?.(messageWithTenant); + } else { + this.onmessage?.(message); + } } // Wait for responses and send them