MCP JavaScript Server
by davlgd
import { logRPC } from './src/logging.js';
import { fetchResource } from './src/utils.js';
import { ResponseFormats } from './src/response-formats.js';
import { PROTOCOL_VERSION, RPC_ERROR_CODES } from './src/constants.js';
/**
* Class representing a MCP server
* @param {Object} infos - The server infos { name, version }
* @param {Object} prompts - The server prompts
* @param {Object} resources - The server resources
* @param {Object} tools - The server tools
* @returns {MCP} - A new MCP server instance
*/
export class MCP {
constructor(infos, prompts, resources, tools) {
this.infos = infos;
this.tools = tools;
this.prompts = prompts;
this.resources = resources;
this.isConnected = false;
this.buffer = '';
this.initializeServer();
}
initializeServer() {
process.stdin.setEncoding('utf8');
process.stdin.on('data', this.handleInput.bind(this));
process.on('SIGTERM', this.handleTermination.bind(this));
}
async handleInput(chunk) {
this.buffer += chunk;
let newlineIndex;
while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) {
const line = this.buffer.slice(0, newlineIndex);
this.buffer = this.buffer.slice(newlineIndex + 1);
if (line.trim()) await this.processRequest(line);
}
}
createResponse(id, result) {
return ResponseFormats.jsonRPC(id, result);
}
createErrorResponse(id, code, message, data) {
return ResponseFormats.error(id, code, message, data);
}
async processRequest(line) {
try {
const request = JSON.parse(line);
await logRPC('request', request);
const response = await this.handleMethod(request);
if (request.id !== undefined) {
await logRPC('response', response);
process.stdout.write(JSON.stringify(response) + '\n');
}
} catch (error) {
const errorResponse = this.createErrorResponse(null, RPC_ERROR_CODES.PARSE_ERROR, 'Parse error', error.message);
await logRPC('error', errorResponse);
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
}
async handleMethod(request) {
const { method, params, id } = request;
const methodHandlers = {
initialize: () => this.handleInitialize(params),
shutdown: () => this.handleShutdown(),
exit: () => this.handleExit(),
'tools/list': () => this.handleToolsList(),
'tools/call': () => this.handleToolsCall(params),
'notifications/cancelled': () => this.handleNotificationsCancelled(),
'notifications/initialized': () => this.handleNotificationsInitialized(),
'ping': () => this.handlePing(),
'prompts/list': () => this.handlePromptsList(),
'prompts/get': () => this.handlePromptsGet(params),
'resources/list': () => this.handleResourcesList(),
'resources/read': () => this.handleResourcesRead(params),
'resources/templates/list': () => this.handleResourcesTemplatesList()
};
const handler = methodHandlers[method];
if (!handler) {
return this.createErrorResponse(id, RPC_ERROR_CODES.METHOD_NOT_FOUND, 'Method not found');
}
return this.createResponse(id, await handler());
}
handlePing() {
return {};
}
handleShutdown() {
this.isConnected = false;
return null;
}
handleExit() {
process.exit(0);
}
handleNotificationsCancelled() {
return null;
}
handleNotificationsInitialized() {
return null;
}
handleResourcesList() {
return ResponseFormats.resources.list(Object.entries(this.resources));
}
async handleResourcesRead(params) {
const { uri } = params;
const resource = Object.values(this.resources).find(r => r.uri === uri);
if (!resource) {
throw new Error('Resource not found');
}
if (uri.startsWith('http')) {
try {
const { content, mimeType } = await fetchResource(uri);
return ResponseFormats.resources.read({
...resource,
mimeType,
content
});
} catch (error) {
throw new Error(`Failed to fetch resource: ${error.message}`);
}
}
return ResponseFormats.resources.read(resource);
}
handleResourcesTemplatesList() {
return ResponseFormats.resources.templates();
}
handlePromptsList() {
return ResponseFormats.prompts.list(Object.entries(this.prompts));
}
handlePromptsGet(params) {
const { name } = params;
const prompt = this.prompts[name];
if (!prompt) {
throw new Error('Prompt not found');
}
return ResponseFormats.prompts.get(prompt);
}
handleInitialize(params) {
if (params?.protocolVersion !== PROTOCOL_VERSION) {
throw new Error('Protocol version not supported');
}
this.isConnected = true;
process.stdin.resume();
return ResponseFormats.initialize(PROTOCOL_VERSION, this.infos);
}
async handleTermination() {
if (this.isConnected) {
await logRPC('info', { message: 'Server shutting down' });
}
process.exit(0);
}
async handleToolsList() {
return ResponseFormats.tools.list(Object.entries(this.tools));
}
async handleToolsCall(params) {
const { name, arguments: args } = params;
const tool = this.tools[name];
if (!tool) {
throw new Error('Tool not found');
}
const result = await tool.handler(args);
return ResponseFormats.tools.call(result);
}
}