@n24q02m/mcp-relay-server 0.1.0 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/build/app.d.ts.map +1 -1
- package/build/app.js +6 -2
- package/build/app.js.map +1 -1
- package/build/middleware/rate-limit.d.ts +2 -1
- package/build/middleware/rate-limit.d.ts.map +1 -1
- package/build/middleware/rate-limit.js +12 -2
- package/build/middleware/rate-limit.js.map +1 -1
- package/build/routes/sessions.d.ts.map +1 -1
- package/build/routes/sessions.js +80 -1
- package/build/routes/sessions.js.map +1 -1
- package/build/store.d.ts +18 -0
- package/build/store.d.ts.map +1 -1
- package/build/store.js +51 -2
- package/build/store.js.map +1 -1
- package/package.json +27 -1
- package/src/app.ts +7 -2
- package/src/middleware/rate-limit.ts +13 -2
- package/src/routes/sessions.ts +116 -1
- package/src/store.ts +61 -2
- package/tests/rate-limit.test.ts +25 -7
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @n24q02m/mcp-relay-server
|
|
2
|
+
|
|
3
|
+
Zero-config MCP credential relay server -- ECDH P-256 + AES-256-GCM, rate-limited, zero-knowledge.
|
|
4
|
+
|
|
5
|
+
The relay server never sees plaintext credentials. It only stores and forwards opaque encrypted blobs between the browser and the MCP server CLI.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @n24q02m/mcp-relay-server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Start the server
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createApp } from "@n24q02m/mcp-relay-server";
|
|
19
|
+
|
|
20
|
+
const app = createApp();
|
|
21
|
+
app.listen(3000, () => {
|
|
22
|
+
console.log("Relay server listening on port 3000");
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Start a local relay (ephemeral, random port)
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { startLocalRelay } from "@n24q02m/mcp-relay-server";
|
|
30
|
+
|
|
31
|
+
const relay = await startLocalRelay("/path/to/pages");
|
|
32
|
+
console.log(`Relay running at ${relay.url}`);
|
|
33
|
+
|
|
34
|
+
// When done:
|
|
35
|
+
relay.close();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Docker
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
docker build -t mcp-relay-server .
|
|
42
|
+
docker run -p 3000:8080 mcp-relay-server
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Environment variables
|
|
46
|
+
|
|
47
|
+
| Variable | Default | Description |
|
|
48
|
+
|----------|---------|-------------|
|
|
49
|
+
| `PORT` | `3000` | Server listen port |
|
|
50
|
+
| `PAGES_DIR` | - | Directory for static relay form pages |
|
|
51
|
+
| `CORS_ORIGIN` | `*` | CORS allowed origin |
|
|
52
|
+
|
|
53
|
+
## Security properties
|
|
54
|
+
|
|
55
|
+
- Zero-knowledge: server never sees plaintext credentials
|
|
56
|
+
- Sessions: 10-minute TTL, one-shot consumption, max 5 per IP
|
|
57
|
+
- Rate-limited API routes
|
|
58
|
+
|
|
59
|
+
## Documentation
|
|
60
|
+
|
|
61
|
+
See the [main repository](https://github.com/n24q02m/mcp-relay-core) for full documentation, architecture, and security properties.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/build/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,SAAS,CAAA;AAI7B,wBAAgB,SAAS,IAAI,OAAO,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,SAAS,CAAA;AAI7B,wBAAgB,SAAS,IAAI,OAAO,CAAC,OAAO,CAqB3C"}
|
package/build/app.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import cors from 'cors';
|
|
2
2
|
import express from 'express';
|
|
3
|
-
import {
|
|
3
|
+
import { mutationLimiter, pollingLimiter } from './middleware/rate-limit.js';
|
|
4
4
|
import { sessionsRouter } from './routes/sessions.js';
|
|
5
5
|
export function createApp() {
|
|
6
6
|
const app = express();
|
|
7
7
|
const corsOrigin = process.env.CORS_ORIGIN ?? '*';
|
|
8
8
|
app.use(cors({ origin: corsOrigin }));
|
|
9
9
|
app.use(express.json({ limit: '1mb' }));
|
|
10
|
-
|
|
10
|
+
// Split rate limits: stricter for mutations, relaxed for polling
|
|
11
|
+
app.use('/api', pollingLimiter); // GET requests (polling) — 120/min
|
|
12
|
+
app.post('/api/sessions', mutationLimiter); // Create session — 30/min
|
|
13
|
+
app.post('/api/sessions/:id/result', mutationLimiter); // Submit credentials — 30/min
|
|
14
|
+
app.post('/api/sessions/:id/skip', mutationLimiter); // Skip — 30/min
|
|
11
15
|
app.use('/api/sessions', sessionsRouter);
|
|
12
16
|
const pagesDir = process.env.PAGES_DIR;
|
|
13
17
|
if (pagesDir) {
|
package/build/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,OAAO,EAAE,CAAA;IAErB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,CAAA;IACjD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,CAAA;IACrC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IAEvC,iEAAiE;IACjE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA,CAAC,mCAAmC;IACnE,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,eAAe,CAAC,CAAA,CAAC,0BAA0B;IACrE,GAAG,CAAC,IAAI,CAAC,0BAA0B,EAAE,eAAe,CAAC,CAAA,CAAC,8BAA8B;IACpF,GAAG,CAAC,IAAI,CAAC,wBAAwB,EAAE,eAAe,CAAC,CAAA,CAAC,gBAAgB;IAEpE,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAA;IAExC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAA;IACtC,IAAI,QAAQ,EAAE,CAAC;QACb,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;IACnC,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const mutationLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
2
|
+
export declare const pollingLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
|
2
3
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,sDAM1B,CAAA;AAIF,eAAO,MAAM,cAAc,sDAMzB,CAAA"}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import rateLimit from 'express-rate-limit';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// Session mutations (create, submit result, skip) — stricter limit
|
|
3
|
+
export const mutationLimiter = rateLimit({
|
|
4
|
+
windowMs: 60 * 1000,
|
|
4
5
|
limit: 30,
|
|
5
6
|
standardHeaders: 'draft-7',
|
|
6
7
|
legacyHeaders: false,
|
|
7
8
|
message: { error: 'Too many requests, please try again later' }
|
|
8
9
|
});
|
|
10
|
+
// Polling endpoints (GET sessions, messages, responses) — higher limit
|
|
11
|
+
// Browser polls every 2s = 30 requests/min per session
|
|
12
|
+
export const pollingLimiter = rateLimit({
|
|
13
|
+
windowMs: 60 * 1000,
|
|
14
|
+
limit: 120,
|
|
15
|
+
standardHeaders: 'draft-7',
|
|
16
|
+
legacyHeaders: false,
|
|
17
|
+
message: { error: 'Too many requests, please try again later' }
|
|
18
|
+
});
|
|
9
19
|
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,oBAAoB,CAAA;AAE1C,MAAM,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,oBAAoB,CAAA;AAE1C,mEAAmE;AACnE,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;IACvC,QAAQ,EAAE,EAAE,GAAG,IAAI;IACnB,KAAK,EAAE,EAAE;IACT,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,OAAO,EAAE,EAAE,KAAK,EAAE,2CAA2C,EAAE;CAChE,CAAC,CAAA;AAEF,uEAAuE;AACvE,uDAAuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG,SAAS,CAAC;IACtC,QAAQ,EAAE,EAAE,GAAG,IAAI;IACnB,KAAK,EAAE,GAAG;IACV,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,OAAO,EAAE,EAAE,KAAK,EAAE,2CAA2C,EAAE;CAChE,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAmBhC,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,OAAO,MAAM,CAAY,CAAA"}
|
package/build/routes/sessions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { createSession, deleteSession, getSession, setSessionResult } from '../store.js';
|
|
2
|
+
import { addMessage, addResponse, createSession, deleteSession, getMessages, getResponses, getSession, setSessionResult, skipSession } from '../store.js';
|
|
3
3
|
function paramId(req) {
|
|
4
4
|
const id = req.params.id;
|
|
5
5
|
return Array.isArray(id) ? id[0] : id;
|
|
@@ -25,6 +25,10 @@ sessionsRouter.get('/:id', (req, res) => {
|
|
|
25
25
|
res.status(404).json({ error: 'Session not found or expired' });
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
+
if (session.skipped) {
|
|
29
|
+
res.status(200).json({ status: 'skipped' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
28
32
|
if (session.result === null) {
|
|
29
33
|
res
|
|
30
34
|
.status(202)
|
|
@@ -51,6 +55,81 @@ sessionsRouter.post('/:id/result', (req, res) => {
|
|
|
51
55
|
}
|
|
52
56
|
res.status(200).json({ ok: true });
|
|
53
57
|
});
|
|
58
|
+
sessionsRouter.post('/:id/skip', (req, res) => {
|
|
59
|
+
const session = getSession(paramId(req));
|
|
60
|
+
if (!session) {
|
|
61
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const success = skipSession(paramId(req));
|
|
65
|
+
if (!success) {
|
|
66
|
+
res.status(409).json({ error: 'Session already has a result or is already skipped' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
res.status(200).json({ ok: true });
|
|
70
|
+
});
|
|
71
|
+
// Server pushes a message to the browser
|
|
72
|
+
sessionsRouter.post('/:id/messages', (req, res) => {
|
|
73
|
+
const { type, text, data } = req.body;
|
|
74
|
+
if (!type || !text) {
|
|
75
|
+
res.status(400).json({ error: 'type and text are required' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const session = getSession(paramId(req));
|
|
79
|
+
if (!session) {
|
|
80
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const messageId = crypto.randomUUID();
|
|
84
|
+
const message = { id: messageId, type, text, data };
|
|
85
|
+
const success = addMessage(paramId(req), message);
|
|
86
|
+
if (!success) {
|
|
87
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
res.status(201).json({ id: messageId });
|
|
91
|
+
});
|
|
92
|
+
// Browser polls for messages from server
|
|
93
|
+
sessionsRouter.get('/:id/messages', (req, res) => {
|
|
94
|
+
const session = getSession(paramId(req));
|
|
95
|
+
if (!session) {
|
|
96
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const after = Number.parseInt(req.query.after, 10);
|
|
100
|
+
const afterIndex = Number.isNaN(after) ? 0 : after;
|
|
101
|
+
const messages = getMessages(paramId(req), afterIndex);
|
|
102
|
+
res.status(200).json({ messages });
|
|
103
|
+
});
|
|
104
|
+
// Browser sends a response to a message
|
|
105
|
+
sessionsRouter.post('/:id/responses', (req, res) => {
|
|
106
|
+
const { messageId, value } = req.body;
|
|
107
|
+
if (!messageId || value === undefined) {
|
|
108
|
+
res.status(400).json({ error: 'messageId and value are required' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const session = getSession(paramId(req));
|
|
112
|
+
if (!session) {
|
|
113
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const success = addResponse(paramId(req), { messageId, value });
|
|
117
|
+
if (!success) {
|
|
118
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
res.status(200).json({ ok: true });
|
|
122
|
+
});
|
|
123
|
+
// Server polls for responses from browser
|
|
124
|
+
sessionsRouter.get('/:id/responses', (req, res) => {
|
|
125
|
+
const session = getSession(paramId(req));
|
|
126
|
+
if (!session) {
|
|
127
|
+
res.status(404).json({ error: 'Session not found or expired' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const responses = getResponses(paramId(req));
|
|
131
|
+
res.status(200).json({ responses });
|
|
132
|
+
});
|
|
54
133
|
sessionsRouter.delete('/:id', (req, res) => {
|
|
55
134
|
deleteSession(paramId(req));
|
|
56
135
|
res.status(204).send();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"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"}
|
package/build/store.d.ts
CHANGED
|
@@ -4,18 +4,36 @@ export interface SessionResult {
|
|
|
4
4
|
iv: string;
|
|
5
5
|
tag: string;
|
|
6
6
|
}
|
|
7
|
+
export interface RelayMessage {
|
|
8
|
+
id: string;
|
|
9
|
+
type: 'info' | 'oauth_device_code' | 'input_required' | 'complete' | 'error';
|
|
10
|
+
text: string;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface RelayResponse {
|
|
14
|
+
messageId: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
7
17
|
export interface Session {
|
|
8
18
|
id: string;
|
|
9
19
|
serverName: string;
|
|
10
20
|
schema: unknown;
|
|
11
21
|
result: SessionResult | null;
|
|
22
|
+
skipped: boolean;
|
|
12
23
|
createdAt: number;
|
|
13
24
|
sourceIp: string;
|
|
25
|
+
messages: RelayMessage[];
|
|
26
|
+
responses: RelayResponse[];
|
|
14
27
|
}
|
|
15
28
|
export declare function getSession(id: string): Session | undefined;
|
|
16
29
|
export declare function createSession(id: string, serverName: string, schema: unknown, sourceIp: string): Session | null;
|
|
17
30
|
export declare function setSessionResult(id: string, result: SessionResult): boolean;
|
|
31
|
+
export declare function skipSession(id: string): boolean;
|
|
18
32
|
export declare function deleteSession(id: string): boolean;
|
|
33
|
+
export declare function addMessage(id: string, message: RelayMessage): boolean;
|
|
34
|
+
export declare function getMessages(id: string, afterIndex?: number): RelayMessage[];
|
|
35
|
+
export declare function addResponse(id: string, response: RelayResponse): boolean;
|
|
36
|
+
export declare function getResponses(id: string): RelayResponse[];
|
|
19
37
|
export declare function startCleanup(): void;
|
|
20
38
|
export declare function stopCleanup(): void;
|
|
21
39
|
export declare function clearAllSessions(): void;
|
package/build/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,UAAU,GAAG,OAAO,CAAA;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,SAAS,EAAE,aAAa,EAAE,CAAA;CAC3B;AAWD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAO1D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAmB/G;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAO3E;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAO/C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAMrE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,YAAY,EAAE,CAK3E;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAMxE;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,EAAE,CAIxD;AAsBD,wBAAgB,YAAY,IAAI,IAAI,CAKnC;AAED,wBAAgB,WAAW,IAAI,IAAI,CAKlC;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|
package/build/store.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const SESSION_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
2
2
|
const CLEANUP_INTERVAL_MS = 60 * 1000; // 60 seconds
|
|
3
|
-
const MAX_SESSIONS_PER_IP =
|
|
3
|
+
const MAX_SESSIONS_PER_IP = 10;
|
|
4
|
+
const MAX_MESSAGES_PER_SESSION = 50;
|
|
5
|
+
const MAX_RESPONSES_PER_SESSION = 50;
|
|
4
6
|
const sessions = new Map();
|
|
5
7
|
let cleanupTimer = null;
|
|
6
8
|
export function getSession(id) {
|
|
@@ -21,8 +23,11 @@ export function createSession(id, serverName, schema, sourceIp) {
|
|
|
21
23
|
serverName,
|
|
22
24
|
schema,
|
|
23
25
|
result: null,
|
|
26
|
+
skipped: false,
|
|
24
27
|
createdAt: Date.now(),
|
|
25
|
-
sourceIp
|
|
28
|
+
sourceIp,
|
|
29
|
+
messages: [],
|
|
30
|
+
responses: []
|
|
26
31
|
};
|
|
27
32
|
sessions.set(id, session);
|
|
28
33
|
return session;
|
|
@@ -33,12 +38,56 @@ export function setSessionResult(id, result) {
|
|
|
33
38
|
return false;
|
|
34
39
|
if (session.result !== null)
|
|
35
40
|
return false;
|
|
41
|
+
if (session.skipped)
|
|
42
|
+
return false;
|
|
36
43
|
session.result = result;
|
|
37
44
|
return true;
|
|
38
45
|
}
|
|
46
|
+
export function skipSession(id) {
|
|
47
|
+
const session = getSession(id);
|
|
48
|
+
if (!session)
|
|
49
|
+
return false;
|
|
50
|
+
if (session.result !== null)
|
|
51
|
+
return false;
|
|
52
|
+
if (session.skipped)
|
|
53
|
+
return false;
|
|
54
|
+
session.skipped = true;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
39
57
|
export function deleteSession(id) {
|
|
40
58
|
return sessions.delete(id);
|
|
41
59
|
}
|
|
60
|
+
export function addMessage(id, message) {
|
|
61
|
+
const session = getSession(id);
|
|
62
|
+
if (!session)
|
|
63
|
+
return false;
|
|
64
|
+
if (session.messages.length >= MAX_MESSAGES_PER_SESSION)
|
|
65
|
+
return false;
|
|
66
|
+
session.messages.push(message);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
export function getMessages(id, afterIndex) {
|
|
70
|
+
const session = getSession(id);
|
|
71
|
+
if (!session)
|
|
72
|
+
return [];
|
|
73
|
+
const start = afterIndex ?? 0;
|
|
74
|
+
return session.messages.slice(start);
|
|
75
|
+
}
|
|
76
|
+
export function addResponse(id, response) {
|
|
77
|
+
const session = getSession(id);
|
|
78
|
+
if (!session)
|
|
79
|
+
return false;
|
|
80
|
+
if (session.responses.length >= MAX_RESPONSES_PER_SESSION)
|
|
81
|
+
return false;
|
|
82
|
+
session.responses.push(response);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
export function getResponses(id) {
|
|
86
|
+
const session = getSession(id);
|
|
87
|
+
if (!session)
|
|
88
|
+
return [];
|
|
89
|
+
return session.responses;
|
|
90
|
+
}
|
|
42
91
|
function countSessionsByIp(ip) {
|
|
43
92
|
const now = Date.now();
|
|
44
93
|
let count = 0;
|
package/build/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n24q02m/mcp-relay-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Zero-config MCP credential relay server — ECDH P-256 + AES-256-GCM, rate-limited, zero-knowledge",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"relay",
|
|
8
|
+
"server",
|
|
9
|
+
"credentials",
|
|
10
|
+
"encryption",
|
|
11
|
+
"zero-knowledge"
|
|
12
|
+
],
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "n24q02m",
|
|
15
|
+
"url": "https://github.com/n24q02m"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/n24q02m/mcp-relay-core.git",
|
|
21
|
+
"directory": "packages/relay-server"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/n24q02m/mcp-relay-core",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/n24q02m/mcp-relay-core/issues"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
4
30
|
"type": "module",
|
|
5
31
|
"main": "build/index.js",
|
|
6
32
|
"scripts": {
|
package/src/app.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cors from 'cors'
|
|
2
2
|
import express from 'express'
|
|
3
|
-
import {
|
|
3
|
+
import { mutationLimiter, pollingLimiter } from './middleware/rate-limit.js'
|
|
4
4
|
import { sessionsRouter } from './routes/sessions.js'
|
|
5
5
|
|
|
6
6
|
export function createApp(): express.Express {
|
|
@@ -9,7 +9,12 @@ export function createApp(): express.Express {
|
|
|
9
9
|
const corsOrigin = process.env.CORS_ORIGIN ?? '*'
|
|
10
10
|
app.use(cors({ origin: corsOrigin }))
|
|
11
11
|
app.use(express.json({ limit: '1mb' }))
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
// Split rate limits: stricter for mutations, relaxed for polling
|
|
14
|
+
app.use('/api', pollingLimiter) // GET requests (polling) — 120/min
|
|
15
|
+
app.post('/api/sessions', mutationLimiter) // Create session — 30/min
|
|
16
|
+
app.post('/api/sessions/:id/result', mutationLimiter) // Submit credentials — 30/min
|
|
17
|
+
app.post('/api/sessions/:id/skip', mutationLimiter) // Skip — 30/min
|
|
13
18
|
|
|
14
19
|
app.use('/api/sessions', sessionsRouter)
|
|
15
20
|
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import rateLimit from 'express-rate-limit'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Session mutations (create, submit result, skip) — stricter limit
|
|
4
|
+
export const mutationLimiter = rateLimit({
|
|
5
|
+
windowMs: 60 * 1000,
|
|
5
6
|
limit: 30,
|
|
6
7
|
standardHeaders: 'draft-7',
|
|
7
8
|
legacyHeaders: false,
|
|
8
9
|
message: { error: 'Too many requests, please try again later' }
|
|
9
10
|
})
|
|
11
|
+
|
|
12
|
+
// Polling endpoints (GET sessions, messages, responses) — higher limit
|
|
13
|
+
// Browser polls every 2s = 30 requests/min per session
|
|
14
|
+
export const pollingLimiter = rateLimit({
|
|
15
|
+
windowMs: 60 * 1000,
|
|
16
|
+
limit: 120,
|
|
17
|
+
standardHeaders: 'draft-7',
|
|
18
|
+
legacyHeaders: false,
|
|
19
|
+
message: { error: 'Too many requests, please try again later' }
|
|
20
|
+
})
|
package/src/routes/sessions.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Request, Response } from 'express'
|
|
2
2
|
import { Router } from 'express'
|
|
3
|
-
import {
|
|
3
|
+
import type { RelayMessage } from '../store.js'
|
|
4
|
+
import {
|
|
5
|
+
addMessage,
|
|
6
|
+
addResponse,
|
|
7
|
+
createSession,
|
|
8
|
+
deleteSession,
|
|
9
|
+
getMessages,
|
|
10
|
+
getResponses,
|
|
11
|
+
getSession,
|
|
12
|
+
setSessionResult,
|
|
13
|
+
skipSession
|
|
14
|
+
} from '../store.js'
|
|
4
15
|
|
|
5
16
|
function paramId(req: Request): string {
|
|
6
17
|
const id = req.params.id
|
|
@@ -40,6 +51,11 @@ sessionsRouter.get('/:id', (req: Request, res: Response) => {
|
|
|
40
51
|
return
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
if (session.skipped) {
|
|
55
|
+
res.status(200).json({ status: 'skipped' })
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
if (session.result === null) {
|
|
44
60
|
res
|
|
45
61
|
.status(202)
|
|
@@ -78,6 +94,105 @@ sessionsRouter.post('/:id/result', (req: Request, res: Response) => {
|
|
|
78
94
|
res.status(200).json({ ok: true })
|
|
79
95
|
})
|
|
80
96
|
|
|
97
|
+
sessionsRouter.post('/:id/skip', (req: Request, res: Response) => {
|
|
98
|
+
const session = getSession(paramId(req))
|
|
99
|
+
if (!session) {
|
|
100
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const success = skipSession(paramId(req))
|
|
105
|
+
if (!success) {
|
|
106
|
+
res.status(409).json({ error: 'Session already has a result or is already skipped' })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.status(200).json({ ok: true })
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Server pushes a message to the browser
|
|
114
|
+
sessionsRouter.post('/:id/messages', (req: Request, res: Response) => {
|
|
115
|
+
const { type, text, data } = req.body as {
|
|
116
|
+
type?: RelayMessage['type']
|
|
117
|
+
text?: string
|
|
118
|
+
data?: Record<string, unknown>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!type || !text) {
|
|
122
|
+
res.status(400).json({ error: 'type and text are required' })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const session = getSession(paramId(req))
|
|
127
|
+
if (!session) {
|
|
128
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const messageId = crypto.randomUUID()
|
|
133
|
+
const message: RelayMessage = { id: messageId, type, text, data }
|
|
134
|
+
const success = addMessage(paramId(req), message)
|
|
135
|
+
if (!success) {
|
|
136
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
res.status(201).json({ id: messageId })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Browser polls for messages from server
|
|
144
|
+
sessionsRouter.get('/:id/messages', (req: Request, res: Response) => {
|
|
145
|
+
const session = getSession(paramId(req))
|
|
146
|
+
if (!session) {
|
|
147
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const after = Number.parseInt(req.query.after as string, 10)
|
|
152
|
+
const afterIndex = Number.isNaN(after) ? 0 : after
|
|
153
|
+
const messages = getMessages(paramId(req), afterIndex)
|
|
154
|
+
res.status(200).json({ messages })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Browser sends a response to a message
|
|
158
|
+
sessionsRouter.post('/:id/responses', (req: Request, res: Response) => {
|
|
159
|
+
const { messageId, value } = req.body as {
|
|
160
|
+
messageId?: string
|
|
161
|
+
value?: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!messageId || value === undefined) {
|
|
165
|
+
res.status(400).json({ error: 'messageId and value are required' })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const session = getSession(paramId(req))
|
|
170
|
+
if (!session) {
|
|
171
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const success = addResponse(paramId(req), { messageId, value })
|
|
176
|
+
if (!success) {
|
|
177
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
res.status(200).json({ ok: true })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Server polls for responses from browser
|
|
185
|
+
sessionsRouter.get('/:id/responses', (req: Request, res: Response) => {
|
|
186
|
+
const session = getSession(paramId(req))
|
|
187
|
+
if (!session) {
|
|
188
|
+
res.status(404).json({ error: 'Session not found or expired' })
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const responses = getResponses(paramId(req))
|
|
193
|
+
res.status(200).json({ responses })
|
|
194
|
+
})
|
|
195
|
+
|
|
81
196
|
sessionsRouter.delete('/:id', (req: Request, res: Response) => {
|
|
82
197
|
deleteSession(paramId(req))
|
|
83
198
|
res.status(204).send()
|
package/src/store.ts
CHANGED
|
@@ -5,18 +5,35 @@ export interface SessionResult {
|
|
|
5
5
|
tag: string
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface RelayMessage {
|
|
9
|
+
id: string
|
|
10
|
+
type: 'info' | 'oauth_device_code' | 'input_required' | 'complete' | 'error'
|
|
11
|
+
text: string
|
|
12
|
+
data?: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RelayResponse {
|
|
16
|
+
messageId: string
|
|
17
|
+
value: string
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export interface Session {
|
|
9
21
|
id: string
|
|
10
22
|
serverName: string
|
|
11
23
|
schema: unknown
|
|
12
24
|
result: SessionResult | null
|
|
25
|
+
skipped: boolean
|
|
13
26
|
createdAt: number
|
|
14
27
|
sourceIp: string
|
|
28
|
+
messages: RelayMessage[]
|
|
29
|
+
responses: RelayResponse[]
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
const SESSION_TTL_MS = 10 * 60 * 1000 // 10 minutes
|
|
18
33
|
const CLEANUP_INTERVAL_MS = 60 * 1000 // 60 seconds
|
|
19
|
-
const MAX_SESSIONS_PER_IP =
|
|
34
|
+
const MAX_SESSIONS_PER_IP = 10
|
|
35
|
+
const MAX_MESSAGES_PER_SESSION = 50
|
|
36
|
+
const MAX_RESPONSES_PER_SESSION = 50
|
|
20
37
|
|
|
21
38
|
const sessions = new Map<string, Session>()
|
|
22
39
|
let cleanupTimer: ReturnType<typeof setInterval> | null = null
|
|
@@ -41,8 +58,11 @@ export function createSession(id: string, serverName: string, schema: unknown, s
|
|
|
41
58
|
serverName,
|
|
42
59
|
schema,
|
|
43
60
|
result: null,
|
|
61
|
+
skipped: false,
|
|
44
62
|
createdAt: Date.now(),
|
|
45
|
-
sourceIp
|
|
63
|
+
sourceIp,
|
|
64
|
+
messages: [],
|
|
65
|
+
responses: []
|
|
46
66
|
}
|
|
47
67
|
sessions.set(id, session)
|
|
48
68
|
return session
|
|
@@ -52,14 +72,53 @@ export function setSessionResult(id: string, result: SessionResult): boolean {
|
|
|
52
72
|
const session = getSession(id)
|
|
53
73
|
if (!session) return false
|
|
54
74
|
if (session.result !== null) return false
|
|
75
|
+
if (session.skipped) return false
|
|
55
76
|
session.result = result
|
|
56
77
|
return true
|
|
57
78
|
}
|
|
58
79
|
|
|
80
|
+
export function skipSession(id: string): boolean {
|
|
81
|
+
const session = getSession(id)
|
|
82
|
+
if (!session) return false
|
|
83
|
+
if (session.result !== null) return false
|
|
84
|
+
if (session.skipped) return false
|
|
85
|
+
session.skipped = true
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
export function deleteSession(id: string): boolean {
|
|
60
90
|
return sessions.delete(id)
|
|
61
91
|
}
|
|
62
92
|
|
|
93
|
+
export function addMessage(id: string, message: RelayMessage): boolean {
|
|
94
|
+
const session = getSession(id)
|
|
95
|
+
if (!session) return false
|
|
96
|
+
if (session.messages.length >= MAX_MESSAGES_PER_SESSION) return false
|
|
97
|
+
session.messages.push(message)
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getMessages(id: string, afterIndex?: number): RelayMessage[] {
|
|
102
|
+
const session = getSession(id)
|
|
103
|
+
if (!session) return []
|
|
104
|
+
const start = afterIndex ?? 0
|
|
105
|
+
return session.messages.slice(start)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function addResponse(id: string, response: RelayResponse): boolean {
|
|
109
|
+
const session = getSession(id)
|
|
110
|
+
if (!session) return false
|
|
111
|
+
if (session.responses.length >= MAX_RESPONSES_PER_SESSION) return false
|
|
112
|
+
session.responses.push(response)
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getResponses(id: string): RelayResponse[] {
|
|
117
|
+
const session = getSession(id)
|
|
118
|
+
if (!session) return []
|
|
119
|
+
return session.responses
|
|
120
|
+
}
|
|
121
|
+
|
|
63
122
|
function countSessionsByIp(ip: string): number {
|
|
64
123
|
const now = Date.now()
|
|
65
124
|
let count = 0
|
package/tests/rate-limit.test.ts
CHANGED
|
@@ -30,20 +30,38 @@ afterAll(async () => {
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
describe('Rate limiting', () => {
|
|
33
|
-
it('rate limit kicks in after 30 requests per minute', async () => {
|
|
34
|
-
//
|
|
33
|
+
it('mutation rate limit kicks in after 30 POST requests per minute', async () => {
|
|
34
|
+
// POST requests go through mutationLimiter (30/min)
|
|
35
35
|
const results: number[] = []
|
|
36
36
|
for (let i = 0; i < 31; i++) {
|
|
37
|
-
const res = await fetch(`${baseUrl}/api/sessions
|
|
37
|
+
const res = await fetch(`${baseUrl}/api/sessions`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ serverName: `test-${i}`, schema: {} })
|
|
41
|
+
})
|
|
38
42
|
results.push(res.status)
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
// First 30 should
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
+
// First 30 should succeed (201 created)
|
|
46
|
+
const successCount = results.filter((s) => s === 201).length
|
|
47
|
+
expect(successCount).toBeLessThanOrEqual(30)
|
|
45
48
|
|
|
46
49
|
// 31st should be rate limited
|
|
47
50
|
expect(results[30]).toBe(429)
|
|
48
51
|
})
|
|
52
|
+
|
|
53
|
+
it('polling rate limit allows 120 GET requests per minute', async () => {
|
|
54
|
+
// GET requests go through pollingLimiter (120/min)
|
|
55
|
+
// Send 31 GETs — all should be 404 (no rate limit at this count)
|
|
56
|
+
const results: number[] = []
|
|
57
|
+
for (let i = 0; i < 31; i++) {
|
|
58
|
+
const res = await fetch(`${baseUrl}/api/sessions/poll-test-${i}`)
|
|
59
|
+
results.push(res.status)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// All 31 should be 404 (not rate limited — limit is 120)
|
|
63
|
+
for (const status of results) {
|
|
64
|
+
expect(status).toBe(404)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
49
67
|
})
|