Skip to content

Commit 1665ec3

Browse files
Merge pull request #3 from SimpleNotificationSystem/development
Publish MCP server as npm package and update dashboard middleware
2 parents 229aff7 + f75cd89 commit 1665ec3

5 files changed

Lines changed: 117 additions & 8 deletions

File tree

dashboard/middleware.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { NextRequest } from "next/server";
33

44
const basePath = process.env.BASE_PATH || "";
55
const SESSION_COOKIE_NAME = "simplens_session";
6+
const NS_API_KEY = process.env.NS_API_KEY || "";
67

78
// Routes that don't require authentication
89
const publicRoutes = [
@@ -112,6 +113,61 @@ async function validateSessionFromRequest(request: NextRequest): Promise<{
112113
}
113114
}
114115

116+
/**
117+
* Validate an API key from the Authorization: Bearer header.
118+
* Uses timing-safe comparison via HMAC to prevent timing attacks.
119+
*/
120+
async function validateApiKeyFromRequest(request: NextRequest): Promise<{ isValid: boolean }> {
121+
try {
122+
if (!NS_API_KEY) {
123+
return { isValid: false };
124+
}
125+
126+
const authHeader = request.headers.get("authorization");
127+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
128+
return { isValid: false };
129+
}
130+
131+
const providedKey = authHeader.slice(7); // Remove "Bearer " prefix
132+
if (!providedKey) {
133+
return { isValid: false };
134+
}
135+
136+
// Timing-safe comparison using HMAC (Edge-compatible via Web Crypto API)
137+
const encoder = new TextEncoder();
138+
const key = await crypto.subtle.importKey(
139+
"raw",
140+
encoder.encode("api-key-compare"),
141+
{ name: "HMAC", hash: "SHA-256" },
142+
false,
143+
["sign"]
144+
);
145+
146+
const [providedMac, expectedMac] = await Promise.all([
147+
crypto.subtle.sign("HMAC", key, encoder.encode(providedKey)),
148+
crypto.subtle.sign("HMAC", key, encoder.encode(NS_API_KEY)),
149+
]);
150+
151+
const providedArr = new Uint8Array(providedMac);
152+
const expectedArr = new Uint8Array(expectedMac);
153+
154+
if (providedArr.length !== expectedArr.length) {
155+
return { isValid: false };
156+
}
157+
158+
let match = true;
159+
for (let i = 0; i < providedArr.length; i++) {
160+
if (providedArr[i] !== expectedArr[i]) {
161+
match = false;
162+
}
163+
}
164+
165+
return { isValid: match };
166+
} catch {
167+
return { isValid: false };
168+
}
169+
}
170+
115171
function isPublicRoute(pathname: string): boolean {
116172
// Remove base path prefix if present
117173
let normalizedPath = pathname;
@@ -198,7 +254,27 @@ export async function middleware(request: NextRequest) {
198254

199255
// STEP 7: Protected routes - require authentication
200256
if (!session.isValid) {
201-
// Redirect to login
257+
// For API routes, fall back to API key (Bearer token) authentication
258+
// This allows programmatic access (e.g. from MCP server) without a session cookie
259+
if (pathname.startsWith("/api/")) {
260+
const apiKeyAuth = await validateApiKeyFromRequest(request);
261+
if (apiKeyAuth.isValid) {
262+
// Valid API key — allow the request through
263+
if (basePath && request.nextUrl.pathname.startsWith(basePath)) {
264+
const url = request.nextUrl.clone();
265+
url.pathname = pathname;
266+
return NextResponse.rewrite(url);
267+
}
268+
return NextResponse.next();
269+
}
270+
// Invalid or missing API key on an API route — return 401 JSON instead of redirect
271+
return NextResponse.json(
272+
{ error: "Unauthorized: valid session or API key required" },
273+
{ status: 401 }
274+
);
275+
}
276+
277+
// Non-API routes: redirect to login
202278
const loginUrl = new URL(`${basePath}/login`, request.url);
203279
loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname);
204280
return NextResponse.redirect(loginUrl);

packages/mcp-server/README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,34 @@ Configure your MCP Client (e.g. Claude Desktop) to connect via HTTP:
5151
}
5252
```
5353

54+
### Streamable HTTP (Local Command)
55+
56+
Run the server locally:
57+
```bash
58+
npx @simplens/mcp
59+
# or: npm start
60+
```
61+
62+
The server starts default at port: `3001`
63+
64+
Then point your MCP client at the local HTTP endpoint and pass headers on every request:
65+
66+
```json
67+
{
68+
"mcpServers": {
69+
"simplens-local-http": {
70+
"type": "streamable-http",
71+
"url": "http://localhost:3001/mcp",
72+
"headers": {
73+
"X-SimpleNS-API-Key": "your-ns-api-key",
74+
"X-SimpleNS-Core-URL": "http://localhost:3000",
75+
"X-SimpleNS-Dashboard-URL": "http://localhost:3002"
76+
}
77+
}
78+
}
79+
}
80+
```
81+
5482
### Local (Stdio) Mode
5583

5684
You can run the server locally if you have SimpleNS running locally.
@@ -61,8 +89,8 @@ Add to your MCP Client config:
6189
{
6290
"mcpServers": {
6391
"simplens-local": {
64-
"command": "node",
65-
"args": ["/path/to/packages/mcp-server/dist/index.js", "--stdio"],
92+
"command": "npx",
93+
"args": ["@simplens/mcp", "--stdio"],
6694
"env": {
6795
"NS_API_KEY": "your-local-api-key",
6896
"SIMPLENS_CORE_URL": "http://localhost:3000",
@@ -84,4 +112,3 @@ Add to your MCP Client config:
84112
| `retry_failure` | Retry a specific failed notification by ID |
85113
| `list_alerts` | List unresolved system alerts (ghost delivery, stuck processing) |
86114
| `resolve_alert` | Dismiss a specific system alert |
87-

packages/mcp-server/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp-server/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
{
2-
"name": "@simplens/mcp-server",
3-
"version": "1.0.0",
2+
"name": "@simplens/mcp",
3+
"version": "1.0.1",
44
"description": "Remote MCP server for SimpleNS notification orchestration engine",
55
"type": "module",
66
"main": "dist/index.js",
77
"bin": {
88
"simplens-mcp": "dist/index.js"
99
},
10+
"files": [
11+
"dist",
12+
"README.md"
13+
],
1014
"scripts": {
1115
"build": "tsc",
1216
"start": "node dist/index.js",

packages/mcp-server/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#!/usr/bin/env node
2+
13
/**
24
* MCP Server Entry Point
35
*

0 commit comments

Comments
 (0)