Sign In

@lexapi/mcp

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lexapi/mcp - npm Package Compare versions

Comparing version
0.1.3
to
0.2.0
+2
dist/http-entry.d.ts
#!/usr/bin/env node
export {};
#!/usr/bin/env node
import { createHttpServer } from './http-server.js';
const portEnv = process.env.PORT ?? '3000';
const port = Number.parseInt(portEnv, 10);
if (!Number.isFinite(port) || port < 1 || port > 65535) {
console.error(`[lexapi-mcp] invalid PORT: ${portEnv}`);
process.exit(1);
}
const server = createHttpServer();
server.listen(port, () => {
console.error(`[lexapi-mcp] HTTP transport listening on :${port} (POST /v1, GET /health)`);
});
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
console.error(`[lexapi-mcp] ${signal} received, shutting down`);
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10_000).unref();
});
}
import { type Server as HttpServer } from 'node:http';
export declare function createHttpServer(): HttpServer;
import { createServer } from 'node:http';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { LexAPIClient, LexAPIError } from './client.js';
import { tools, toolsByName } from './tools.js';
import { PACKAGE_VERSION } from './version.js';
const PACKAGE_NAME = 'lexapi-mcp';
const MCP_PATH = '/v1';
function extractBearer(req) {
const header = req.headers['authorization'];
if (!header || typeof header !== 'string')
return null;
const match = header.match(/^Bearer\s+(.+)$/i);
return match ? match[1].trim() : null;
}
async function readJsonBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
// 1 MB request-body cap — MCP JSON-RPC messages are tiny; anything larger is abuse.
if (chunks.reduce((n, c) => n + c.length, 0) > 1_000_000) {
throw new Error('request body too large');
}
}
const text = Buffer.concat(chunks).toString('utf8');
if (!text)
return undefined;
return JSON.parse(text);
}
function buildMcpServer(apiKey) {
const baseUrl = process.env.LEXAPI_BASE_URL;
const client = new LexAPIClient({
apiKey,
...(baseUrl ? { baseUrl } : {}),
});
const server = new Server({ name: PACKAGE_NAME, version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: tools.map(({ name, description, inputSchema, annotations }) => ({
name,
description,
inputSchema,
annotations,
})),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
const tool = toolsByName[name];
if (!tool) {
return {
isError: true,
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
};
}
try {
const result = await tool.handler(args, client);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
catch (err) {
if (err instanceof LexAPIError) {
return {
isError: true,
content: [
{
type: 'text',
text: `LexAPI error (${err.status} ${err.slug}): ${err.message}`,
},
],
};
}
const message = err instanceof Error ? err.message : String(err);
return {
isError: true,
content: [{ type: 'text', text: `Tool execution failed: ${message}` }],
};
}
});
return server;
}
function writeJson(res, status, body) {
res.writeHead(status, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, private',
});
res.end(JSON.stringify(body));
}
async function handleMcp(req, res) {
const apiKey = extractBearer(req);
if (!apiKey) {
writeJson(res, 401, {
error: {
code: 'unauthorized',
message: 'Missing or invalid Authorization header. Send: Authorization: Bearer <your LexAPI key>. Get a key at https://lex-api.com/dashboard.',
},
});
return;
}
let body;
try {
body = await readJsonBody(req);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
writeJson(res, 400, { error: { code: 'bad_request', message } });
return;
}
// Defense-in-depth headers — Cloudflare + intermediate proxies should not
// buffer streamed responses, and per-user MCP responses must never be cached.
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-store, private');
const mcpServer = buildMcpServer(apiKey);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on('close', () => {
transport.close().catch(() => { });
mcpServer.close().catch(() => { });
});
await mcpServer.connect(transport);
await transport.handleRequest(req, res, body);
}
export function createHttpServer() {
return createServer(async (req, res) => {
try {
// CORS preflight — MCP clients called from browsers need this.
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version',
'Access-Control-Max-Age': '86400',
});
res.end();
return;
}
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
if (req.method === 'GET' && req.url === '/health') {
writeJson(res, 200, {
status: 'ok',
name: PACKAGE_NAME,
version: PACKAGE_VERSION,
tools: tools.length,
});
return;
}
if (req.url === MCP_PATH || req.url === MCP_PATH + '/') {
await handleMcp(req, res);
return;
}
writeJson(res, 404, { error: { code: 'not_found', message: `No route for ${req.method} ${req.url}` } });
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (!res.headersSent) {
writeJson(res, 500, { error: { code: 'internal_error', message } });
}
else {
res.end();
}
}
});
}
export declare const PACKAGE_VERSION: string;
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
// Read version from package.json at load time so it can't drift from the
// published version. Works in all deploy shapes: npm tarball, source dev,
// Docker (dist/ sits next to package.json in every case).
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
export const PACKAGE_VERSION = pkg.version;
+1
-1

@@ -7,4 +7,4 @@ #!/usr/bin/env node

import { tools, toolsByName } from './tools.js';
import { PACKAGE_VERSION } from './version.js';
const PACKAGE_NAME = 'lexapi-mcp';
const PACKAGE_VERSION = '0.1.3';
const apiKey = process.env.LEXAPI_API_KEY;

@@ -11,0 +11,0 @@ if (!apiKey) {

{
"name": "@lexapi/mcp",
"version": "0.1.3",
"version": "0.2.0",
"mcpName": "io.github.Lex-API/lexapi-mcp",

@@ -20,2 +20,3 @@ "description": "Model Context Protocol server for LexAPI — query EUR-Lex and EU case law from Claude, Cursor, and other MCP-enabled clients.",

"start": "node dist/index.js",
"start:http": "node dist/http-entry.js",
"clean": "rm -rf dist",

@@ -55,4 +56,5 @@ "prepublishOnly": "npm run clean && npm run build"

"hono": "^4.12.27",
"qs": "^6.15.3"
"qs": "^6.15.3",
"body-parser": "^2.3.0"
}
}
# LexAPI MCP
[Model Context Protocol](https://modelcontextprotocol.io) server for [LexAPI](https://lex-api.com) — query EUR-Lex, EU case law, and the citation graph from Claude, Cursor, and other MCP-enabled clients.
[Model Context Protocol](https://modelcontextprotocol.io) server for [LexAPI](https://lex-api.com/?ref=npm-mcp) — query EUR-Lex, EU case law, and the citation graph from Claude, Cursor, and other MCP-enabled clients.

@@ -9,3 +9,3 @@ Install once, get an API key, and ask your AI assistant: *"summarize Article 17 of the GDPR"* or *"which regulations amend Directive 95/46/EC?"* — the model calls LexAPI directly.

You'll need a LexAPI API key. Create one for free at [lex-api.com/dashboard](https://lex-api.com/dashboard) (50 calls/day on the FREE tier).
You'll need a LexAPI API key. Create one for free at [lex-api.com/dashboard](https://lex-api.com/dashboard?ref=npm-mcp) (500 credits/month on the FREE tier, no card required).

@@ -61,3 +61,3 @@ ### Claude Desktop

|---|---|---|
| `LEXAPI_API_KEY` | *(required)* | Your API key. Get one at [lex-api.com/dashboard](https://lex-api.com/dashboard). |
| `LEXAPI_API_KEY` | *(required)* | Your API key. Get one at [lex-api.com/dashboard](https://lex-api.com/dashboard?ref=npm-mcp). |
| `LEXAPI_BASE_URL` | `https://lex-api.com/api/v1` | Override for self-hosted or staging. |

@@ -64,0 +64,0 @@