@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 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
@@ -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,CAgB3C"}
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 { rateLimiter } from './middleware/rate-limit.js';
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
- app.use(rateLimiter);
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,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,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;IACvC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAEpB,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
+ {"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 rateLimiter: import("express-rate-limit").RateLimitRequestHandler;
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":"AAEA,eAAO,MAAM,WAAW,sDAMtB,CAAA"}
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
- export const rateLimiter = rateLimit({
3
- windowMs: 60 * 1000, // 1 minute
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,WAAW,GAAG,SAAS,CAAC;IACnC,QAAQ,EAAE,EAAE,GAAG,IAAI,EAAE,WAAW;IAChC,KAAK,EAAE,EAAE;IACT,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,KAAK;IACpB,OAAO,EAAE,EAAE,KAAK,EAAE,2CAA2C,EAAE;CAChE,CAAC,CAAA"}
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;AAQhC,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,OAAO,MAAM,CAAY,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"}
@@ -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":"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"}
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;
@@ -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;CACjB;AASD,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,CAgB/G;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAM3E;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjD;AAsBD,wBAAgB,YAAY,IAAI,IAAI,CAKnC;AAED,wBAAgB,WAAW,IAAI,IAAI,CAKlC;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
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 = 5;
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;
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
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": "0.1.0",
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 { rateLimiter } from './middleware/rate-limit.js'
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
- app.use(rateLimiter)
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
- export const rateLimiter = rateLimit({
4
- windowMs: 60 * 1000, // 1 minute
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
+ })
@@ -1,6 +1,17 @@
1
1
  import type { Request, Response } from 'express'
2
2
  import { Router } from 'express'
3
- import { createSession, deleteSession, getSession, setSessionResult } from '../store.js'
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 = 5
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
@@ -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
- // Send 30 requests (all should succeed with 404 since no session exists)
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/rate-test-${i}`)
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 be 404 (valid response, session not found)
42
- for (let i = 0; i < 30; i++) {
43
- expect(results[i]).toBe(404)
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
  })