From 8e270024b6638c1962dbc656cb7a56b1814b2eaa Mon Sep 17 00:00:00 2001 From: Angel Garcia Date: Tue, 9 Jun 2026 16:18:00 -0600 Subject: [PATCH] Fix package root entrypoint --- bin/server.js | 142 +--------------------------------------- index.js | 132 +++++++++++++++++++++++++++++++++++++ package-lock.json | 7 +- package.json | 2 +- test/entrypoint.test.js | 43 ++++++++++++ 5 files changed, 184 insertions(+), 142 deletions(-) create mode 100644 index.js create mode 100644 test/entrypoint.test.js diff --git a/bin/server.js b/bin/server.js index 1aaa2cb..65a735d 100755 --- a/bin/server.js +++ b/bin/server.js @@ -1,148 +1,12 @@ #!/usr/bin/env node -import express from 'express'; -import cors from 'cors'; -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath, URL } from 'url'; - -// Helper to determine __dirname in ES modules. -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const app = express(); - -// Enable CORS and JSON parsing middleware. -app.use(cors()); -app.use(express.json()); - -// Helper functions for JSON-RPC responses. -function createResponse (id, result) { - return { - jsonrpc: "2.0", - id: id, - result: result - }; -} - -function createErrorResponse (id, code, message) { - return { - jsonrpc: "2.0", - id: id, - error: { - code: code, - message: message - } - }; -} - -// Load all plugins from both the plugins and private-plugins folders. -const plugins = new Map(); -const pluginDirs = [ - new URL('../plugins/', import.meta.url), - new URL('../private-plugins/', import.meta.url) -]; - -try { - for (const pluginsDir of pluginDirs) { - try { - const files = await fs.readdir(pluginsDir); - for (const file of files) { - if (file.endsWith('.js')) { - const moduleUrl = new URL(file, pluginsDir); - const plugin = await import(moduleUrl.href); - if (typeof plugin.tool === 'function') { - const toolConfig = plugin.tool(); - plugins.set(toolConfig.name, { - handler: toolConfig.fn, - capability: { - description: toolConfig.description, - params: { - type: "object", - properties: Object.fromEntries( - toolConfig.inputs?.map(input => [ - input.name, - { - type: input.type, - description: input.description - } - ]) || [] - ) - } - } - }); - console.log(`Loaded plugin: ${toolConfig.name}`); - } else { - console.warn(`Skipping plugin file ${file} (missing 'tool' function)`); - } - } - } - } catch (err) { - // If directory doesn't exist, just continue - if (err.code === 'ENOENT') { - console.log(`Optional plugin directory not found: ${pluginsDir}`); - continue; - } - throw err; - } - } -} catch (err) { - console.error("Error loading plugins:", err); -} - -// Handle POST requests to the root endpoint. -app.post('/', async (req, res) => { - try { - const message = req.body; - console.log("Received message:", message); - - // Validate JSON-RPC request. - if (!message.jsonrpc || message.jsonrpc !== "2.0") { - return res.json(createErrorResponse(message.id, -32600, "Invalid JSON-RPC request")); - } - if (!message.method) { - return res.json(createErrorResponse(message.id, -32600, "Method is required")); - } - - // Special handling for the "initialize" method: - // List available capabilities from the loaded plugins. - if (message.method === "initialize") { - const capabilities = {}; - plugins.forEach((plugin, method) => { - if (plugin.capability) { - capabilities[method] = plugin.capability; - } - }); - const response = { - capabilities, - serverInfo: { - name: "Pluggable MCP Server", - version: "1.0.0" - } - }; - return res.json(createResponse(message.id, response)); - } - - // Lookup the plugin for the requested method. - const plugin = plugins.get(message.method); - if (!plugin) { - return res.json(createErrorResponse(message.id, -32601, "Method not found")); - } - - // Execute the plugin's handler. - // The handler may be synchronous or return a Promise. - const result = await plugin.handler(message.params); - res.json(createResponse(message.id, result)); - } catch (err) { - console.error("Error processing request:", err); - res.status(500).json(createErrorResponse(null, -32603, "Internal server error")); - } -}); +import { createMcpApp } from '../index.js'; const DEFAULT_PORT = 4333; -// Listen on port 4333. const PORT = process.env.PORT || DEFAULT_PORT; +const app = await createMcpApp(); + app.listen(PORT, '0.0.0.0', () => { console.log(`Server listening on port ${PORT}`); }); diff --git a/index.js b/index.js new file mode 100644 index 0000000..a6d8076 --- /dev/null +++ b/index.js @@ -0,0 +1,132 @@ +import cors from 'cors'; +import express from 'express'; +import fs from 'fs/promises'; + +function createResponse(id, result) { + return { + jsonrpc: "2.0", + id, + result + }; +} + +function createErrorResponse(id, code, message) { + return { + jsonrpc: "2.0", + id, + error: { + code, + message + } + }; +} + +async function loadPlugins(pluginDirs, logger) { + const plugins = new Map(); + + for (const pluginsDir of pluginDirs) { + try { + const files = await fs.readdir(pluginsDir); + for (const file of files) { + if (!file.endsWith('.js')) { + continue; + } + + const moduleUrl = new URL(file, pluginsDir); + const plugin = await import(moduleUrl.href); + if (typeof plugin.tool !== 'function') { + logger.warn(`Skipping plugin file ${file} (missing 'tool' function)`); + continue; + } + + const toolConfig = plugin.tool(); + plugins.set(toolConfig.name, { + handler: toolConfig.fn, + capability: { + description: toolConfig.description, + params: { + type: "object", + properties: Object.fromEntries( + toolConfig.inputs?.map((input) => [ + input.name, + { + type: input.type, + description: input.description + } + ]) || [] + ) + } + } + }); + logger.log(`Loaded plugin: ${toolConfig.name}`); + } + } catch (err) { + if (err.code === 'ENOENT') { + logger.log(`Optional plugin directory not found: ${pluginsDir}`); + continue; + } + throw err; + } + } + + return plugins; +} + +export async function createMcpApp(options = {}) { + const logger = options.logger ?? console; + const pluginDirs = options.pluginDirs ?? [ + new URL('./plugins/', import.meta.url), + new URL('./private-plugins/', import.meta.url) + ]; + const plugins = await loadPlugins(pluginDirs, logger); + const app = express(); + + app.use(cors()); + app.use(express.json()); + + app.post('/', async (req, res) => { + try { + const message = req.body; + logger.log("Received message:", message); + + if (!message.jsonrpc || message.jsonrpc !== "2.0") { + return res.json(createErrorResponse(message.id, -32600, "Invalid JSON-RPC request")); + } + if (!message.method) { + return res.json(createErrorResponse(message.id, -32600, "Method is required")); + } + + if (message.method === "initialize") { + const capabilities = {}; + plugins.forEach((plugin, method) => { + if (plugin.capability) { + capabilities[method] = plugin.capability; + } + }); + const response = { + capabilities, + serverInfo: { + name: "Pluggable MCP Server", + version: "1.0.0" + } + }; + return res.json(createResponse(message.id, response)); + } + + const plugin = plugins.get(message.method); + if (!plugin) { + return res.json(createErrorResponse(message.id, -32601, "Method not found")); + } + + const result = await plugin.handler(message.params); + return res.json(createResponse(message.id, result)); + } catch (err) { + logger.error("Error processing request:", err); + return res.status(500).json(createErrorResponse(null, -32603, "Internal server error")); + } + }); + + return app; +} + +export { createErrorResponse, createResponse }; diff --git a/package-lock.json b/package-lock.json index 82ec436..34813bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,20 @@ { "name": "mcp-server", - "version": "0.0.1", + "version": "0.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mcp-server", - "version": "0.0.1", + "version": "0.0.9", "license": "MIT", "dependencies": { "axios": "^1.7.9", "cors": "^2.8.5", "express": "^4.21.2" + }, + "bin": { + "mcp-server": "bin/server.js" } }, "node_modules/accepts": { diff --git a/package.json b/package.json index 81a5512..6a7c714 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "mcp server", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test" }, "bin": { "mcp-server": "bin/server.js" diff --git a/test/entrypoint.test.js b/test/entrypoint.test.js new file mode 100644 index 0000000..03906f6 --- /dev/null +++ b/test/entrypoint.test.js @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createMcpApp } from '../index.js'; + +const silentLogger = { + error() {}, + log() {}, + warn() {} +}; + +test('root entrypoint exports an app factory', async () => { + assert.equal(typeof createMcpApp, 'function'); + const app = await createMcpApp({ logger: silentLogger }); + assert.equal(typeof app.listen, 'function'); +}); + +test('created app exposes loaded plugin capabilities', async (t) => { + const app = await createMcpApp({ logger: silentLogger }); + const server = app.listen(0, '127.0.0.1'); + t.after(() => server.close()); + + await new Promise((resolve) => server.once('listening', resolve)); + const { port } = server.address(); + const response = await fetch(`http://127.0.0.1:${port}/`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize' + }) + }); + const body = await response.json(); + + assert.equal(response.status, 200); + assert.equal(body.jsonrpc, '2.0'); + assert.equal(body.id, 1); + assert.equal(body.result.serverInfo.name, 'Pluggable MCP Server'); + assert.ok(body.result.capabilities.echo); + assert.ok(body.result.capabilities.helloworld); +});