Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion dashboard/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 30 additions & 3 deletions packages/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
Expand All @@ -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 |

4 changes: 2 additions & 2 deletions packages/mcp-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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"
Comment on lines +10 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Build dist before publishing npm package

The package now whitelists only dist and README.md, but there is still no prepack/prepare step to run tsc, so publishing from a clean checkout (or CI) produces a tarball without dist/index.js; in that state, both main and bin point to a missing file and npx @simplens/mcp cannot start. This is a release blocker for any publish flow that does not manually build first.

Useful? React with 👍 / 👎.

],
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env node

/**
* MCP Server Entry Point
*
Expand Down