server.ts•4.26 kB
import { 
  McpRequest, 
  McpResponse, 
  ServerInfo,
  ListToolsRequest,
  ListResourcesRequest,
  CallToolRequest,
  ReadResourceRequest,
  Tool,
  Resource
} from './types.js';
import { McpError, Errors } from './errors.js';
import { randomUUID } from 'crypto';
export type RequestHandler<T = any, R = any> = (request: McpRequest<T>) => Promise<R>;
export class Server {
  private handlers: Map<string, RequestHandler> = new Map();
  private info: ServerInfo;
  private transport: Transport | null = null;
  constructor(info: ServerInfo) {
    this.info = info;
    this.setupDefaultHandlers();
  }
  private setupDefaultHandlers() {
    // List available tools
    this.setRequestHandler('list_tools', async () => ({
      tools: Array.from(this.handlers.entries())
        .filter(([method]) => method.startsWith('tool:'))
        .map(([method]) => ({
          name: method.replace('tool:', ''),
          description: 'Tool description',
          inputSchema: {}
        }))
    }));
    // List available resources
    this.setRequestHandler('list_resources', async () => ({
      resources: Array.from(this.handlers.entries())
        .filter(([method]) => method.startsWith('resource:'))
        .map(([method]) => ({
          uri: method.replace('resource:', ''),
          name: 'Resource name',
          description: 'Resource description'
        }))
    }));
  }
  public setRequestHandler<T = any, R = any>(method: string, handler: RequestHandler<T, R>) {
    this.handlers.set(method, handler);
  }
  public async handleRequest(request: McpRequest): Promise<McpResponse> {
    try {
      if (!request.jsonrpc || request.jsonrpc !== '2.0') {
        throw Errors.InvalidRequest('Invalid JSON-RPC version');
      }
      if (!request.method || typeof request.method !== 'string') {
        throw Errors.InvalidRequest('Invalid method');
      }
      // Generate a request ID if none provided
      const requestId = request.id ?? randomUUID();
      const handler = this.handlers.get(request.method);
      if (!handler) {
        throw Errors.MethodNotFound(`Method not found: ${request.method}`);
      }
      const result = await handler(request);
      return {
        jsonrpc: '2.0',
        result,
        id: requestId
      };
    } catch (error) {
      if (McpError.isInstance(error)) {
        return {
          jsonrpc: '2.0',
          error: error.toJSON(),
          id: request.id ?? randomUUID()
        };
      }
      return {
        jsonrpc: '2.0',
        error: Errors.InternalError(error instanceof Error ? error.message : 'Internal error').toJSON(),
        id: request.id ?? randomUUID()
      };
    }
  }
  public async connect(transport: Transport) {
    this.transport = transport;
    await transport.connect(this.handleRequest.bind(this));
  }
  public async close() {
    if (this.transport) {
      await this.transport.close();
      this.transport = null;
    }
  }
}
export interface Transport {
  connect(handler: (request: McpRequest) => Promise<McpResponse>): Promise<void>;
  close(): Promise<void>;
}
export class StdioTransport implements Transport {
  private handler: ((request: McpRequest) => Promise<McpResponse>) | null = null;
  private requestId = 0;
  async connect(handler: (request: McpRequest) => Promise<McpResponse>) {
    this.handler = handler;
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', async (data) => {
      try {
        const request = JSON.parse(data.toString()) as McpRequest;
        const response = await this.handler!(request);
        process.stdout.write(JSON.stringify(response) + '\n');
      } catch (error) {
        const response: McpResponse = {
          jsonrpc: '2.0',
          error: Errors.ParseError(error instanceof Error ? error.message : 'Parse error').toJSON(),
          id: String(this.requestId++)
        };
        process.stdout.write(JSON.stringify(response) + '\n');
      }
    });
    // Handle process termination
    process.on('SIGINT', () => this.close());
    process.on('SIGTERM', () => this.close());
  }
  async close() {
    this.handler = null;
    process.stdin.removeAllListeners();
    process.removeAllListeners('SIGINT');
    process.removeAllListeners('SIGTERM');
  }
}