From c5ea87650df274e7d51f6f3dc94e82c55be8ca14 Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Thu, 26 Feb 2026 18:33:35 +0530 Subject: [PATCH 1/2] published mcp server as a npm package (@simplens/mcp) to access in both stdio and streamable-http mode using the npm package --- packages/mcp-server/README.md | 33 ++++++++++++++++++++++++--- packages/mcp-server/package-lock.json | 4 ++-- packages/mcp-server/package.json | 8 +++++-- packages/mcp-server/src/index.ts | 2 ++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index 5904dd0..882d69d 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -51,6 +51,34 @@ Configure your MCP Client (e.g. Claude Desktop) to connect via HTTP: } ``` +### Streamable HTTP (Local Command) + +Run the server locally: +```bash +npx @simplens/mcp +# or: npm start +``` + +The server starts default at port: `3001` + +Then point your MCP client at the local HTTP endpoint and pass headers on every request: + +```json +{ + "mcpServers": { + "simplens-local-http": { + "type": "streamable-http", + "url": "http://localhost:3001/mcp", + "headers": { + "X-SimpleNS-API-Key": "your-ns-api-key", + "X-SimpleNS-Core-URL": "http://localhost:3000", + "X-SimpleNS-Dashboard-URL": "http://localhost:3002" + } + } + } +} +``` + ### Local (Stdio) Mode You can run the server locally if you have SimpleNS running locally. @@ -61,8 +89,8 @@ Add to your MCP Client config: { "mcpServers": { "simplens-local": { - "command": "node", - "args": ["/path/to/packages/mcp-server/dist/index.js", "--stdio"], + "command": "npx", + "args": ["@simplens/mcp", "--stdio"], "env": { "NS_API_KEY": "your-local-api-key", "SIMPLENS_CORE_URL": "http://localhost:3000", @@ -84,4 +112,3 @@ Add to your MCP Client config: | `retry_failure` | Retry a specific failed notification by ID | | `list_alerts` | List unresolved system alerts (ghost delivery, stuck processing) | | `resolve_alert` | Dismiss a specific system alert | - diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json index 4dae04a..aa53d05 100644 --- a/packages/mcp-server/package-lock.json +++ b/packages/mcp-server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@simplens/mcp-server", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@simplens/mcp-server", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 9435186..d0ed565 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,12 +1,16 @@ { - "name": "@simplens/mcp-server", - "version": "1.0.0", + "name": "@simplens/mcp", + "version": "1.0.1", "description": "Remote MCP server for SimpleNS notification orchestration engine", "type": "module", "main": "dist/index.js", "bin": { "simplens-mcp": "dist/index.js" }, + "files": [ + "dist", + "README.md" + ], "scripts": { "build": "tsc", "start": "node dist/index.js", diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index ef1ec02..f92e7bf 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /** * MCP Server Entry Point * From f75cd8956842d1703d5409490d9be0c6ef4f48f5 Mon Sep 17 00:00:00 2001 From: Adhish-Krishna Date: Thu, 26 Feb 2026 19:58:06 +0530 Subject: [PATCH 2/2] update dashboard middleware to check for authorization header in requests for /api requests to allow requests from mcp server --- dashboard/middleware.ts | 78 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/dashboard/middleware.ts b/dashboard/middleware.ts index 69738e4..c8d2ca4 100644 --- a/dashboard/middleware.ts +++ b/dashboard/middleware.ts @@ -3,6 +3,7 @@ import type { NextRequest } from "next/server"; const basePath = process.env.BASE_PATH || ""; const SESSION_COOKIE_NAME = "simplens_session"; +const NS_API_KEY = process.env.NS_API_KEY || ""; // Routes that don't require authentication const publicRoutes = [ @@ -112,6 +113,61 @@ async function validateSessionFromRequest(request: NextRequest): Promise<{ } } +/** + * Validate an API key from the Authorization: Bearer header. + * Uses timing-safe comparison via HMAC to prevent timing attacks. + */ +async function validateApiKeyFromRequest(request: NextRequest): Promise<{ isValid: boolean }> { + try { + if (!NS_API_KEY) { + return { isValid: false }; + } + + const authHeader = request.headers.get("authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return { isValid: false }; + } + + const providedKey = authHeader.slice(7); // Remove "Bearer " prefix + if (!providedKey) { + return { isValid: false }; + } + + // Timing-safe comparison using HMAC (Edge-compatible via Web Crypto API) + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode("api-key-compare"), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + + const [providedMac, expectedMac] = await Promise.all([ + crypto.subtle.sign("HMAC", key, encoder.encode(providedKey)), + crypto.subtle.sign("HMAC", key, encoder.encode(NS_API_KEY)), + ]); + + const providedArr = new Uint8Array(providedMac); + const expectedArr = new Uint8Array(expectedMac); + + if (providedArr.length !== expectedArr.length) { + return { isValid: false }; + } + + let match = true; + for (let i = 0; i < providedArr.length; i++) { + if (providedArr[i] !== expectedArr[i]) { + match = false; + } + } + + return { isValid: match }; + } catch { + return { isValid: false }; + } +} + function isPublicRoute(pathname: string): boolean { // Remove base path prefix if present let normalizedPath = pathname; @@ -198,7 +254,27 @@ export async function middleware(request: NextRequest) { // STEP 7: Protected routes - require authentication if (!session.isValid) { - // Redirect to login + // For API routes, fall back to API key (Bearer token) authentication + // This allows programmatic access (e.g. from MCP server) without a session cookie + if (pathname.startsWith("/api/")) { + const apiKeyAuth = await validateApiKeyFromRequest(request); + if (apiKeyAuth.isValid) { + // Valid API key — allow the request through + if (basePath && request.nextUrl.pathname.startsWith(basePath)) { + const url = request.nextUrl.clone(); + url.pathname = pathname; + return NextResponse.rewrite(url); + } + return NextResponse.next(); + } + // Invalid or missing API key on an API route — return 401 JSON instead of redirect + return NextResponse.json( + { error: "Unauthorized: valid session or API key required" }, + { status: 401 } + ); + } + + // Non-API routes: redirect to login const loginUrl = new URL(`${basePath}/login`, request.url); loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname); return NextResponse.redirect(loginUrl);