@honeyfield/rent2b-mcp 1.1.0 → 1.2.1

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.
Files changed (54) hide show
  1. package/README.md +14 -0
  2. package/dist/api-client.js +14 -5
  3. package/dist/api-client.js.map +1 -1
  4. package/dist/api-client.test.d.ts +1 -0
  5. package/dist/api-client.test.js +41 -0
  6. package/dist/api-client.test.js.map +1 -0
  7. package/dist/auth/authorize-page.d.ts +18 -0
  8. package/dist/auth/authorize-page.js +60 -0
  9. package/dist/auth/authorize-page.js.map +1 -0
  10. package/dist/auth/authorize-page.test.d.ts +1 -0
  11. package/dist/auth/authorize-page.test.js +37 -0
  12. package/dist/auth/authorize-page.test.js.map +1 -0
  13. package/dist/auth/http-oauth.d.ts +28 -0
  14. package/dist/auth/http-oauth.js +93 -0
  15. package/dist/auth/http-oauth.js.map +1 -0
  16. package/dist/auth/oauth-config.d.ts +6 -0
  17. package/dist/auth/oauth-config.js +16 -0
  18. package/dist/auth/oauth-config.js.map +1 -0
  19. package/dist/auth/oauth-config.test.d.ts +1 -0
  20. package/dist/auth/oauth-config.test.js +25 -0
  21. package/dist/auth/oauth-config.test.js.map +1 -0
  22. package/dist/auth/provider.d.ts +28 -0
  23. package/dist/auth/provider.js +127 -0
  24. package/dist/auth/provider.js.map +1 -0
  25. package/dist/auth/provider.test.d.ts +1 -0
  26. package/dist/auth/provider.test.js +80 -0
  27. package/dist/auth/provider.test.js.map +1 -0
  28. package/dist/auth/stores.d.ts +27 -0
  29. package/dist/auth/stores.js +39 -0
  30. package/dist/auth/stores.js.map +1 -0
  31. package/dist/auth/stores.test.d.ts +1 -0
  32. package/dist/auth/stores.test.js +41 -0
  33. package/dist/auth/stores.test.js.map +1 -0
  34. package/dist/auth/token-crypto.d.ts +14 -0
  35. package/dist/auth/token-crypto.js +50 -0
  36. package/dist/auth/token-crypto.js.map +1 -0
  37. package/dist/auth/token-crypto.test.d.ts +1 -0
  38. package/dist/auth/token-crypto.test.js +40 -0
  39. package/dist/auth/token-crypto.test.js.map +1 -0
  40. package/dist/config.d.ts +11 -0
  41. package/dist/config.js +24 -4
  42. package/dist/config.js.map +1 -1
  43. package/dist/generator/openapi-parser.js +21 -0
  44. package/dist/generator/openapi-parser.js.map +1 -1
  45. package/dist/generator/tool-generator.js +19 -1
  46. package/dist/generator/tool-generator.js.map +1 -1
  47. package/dist/generator/tool-generator.test.d.ts +1 -0
  48. package/dist/generator/tool-generator.test.js +35 -0
  49. package/dist/generator/tool-generator.test.js.map +1 -0
  50. package/dist/index.js +136 -60
  51. package/dist/index.js.map +1 -1
  52. package/dist/overrides/tool-overrides.json +60 -0
  53. package/generated/tools.json +1395 -215
  54. package/package.json +8 -3
package/README.md CHANGED
@@ -58,6 +58,20 @@ npx @honeyfield/rent2b-mcp --http --port=8080
58
58
 
59
59
  This exposes a single `/mcp` endpoint supporting POST (JSON-RPC messages), GET (SSE notifications), and DELETE (session termination). Sessions are tracked server-side via the `Mcp-Session-Id` header.
60
60
 
61
+ **Authentication:** In HTTP and SSE mode, each client sends their own API key per request via header:
62
+
63
+ ```
64
+ Authorization: Bearer r2b_your-api-key
65
+ ```
66
+
67
+ or:
68
+
69
+ ```
70
+ X-API-Key: r2b_your-api-key
71
+ ```
72
+
73
+ The API key is validated on session creation. The `RENT2B_API_KEY` environment variable is **not required** in HTTP/SSE mode.
74
+
61
75
  ### SSE Mode (Legacy)
62
76
 
63
77
  For SSE transport (e.g. for older web-based MCP clients):
@@ -5,6 +5,11 @@ export class Rent2bApiClient {
5
5
  organizationId = null;
6
6
  constructor(config) {
7
7
  this.config = config;
8
+ // Reuse an org already resolved by the auth layer to avoid a redundant
9
+ // GET /zapier/me on every request.
10
+ if (config.organizationId != null) {
11
+ this.organizationId = config.organizationId;
12
+ }
8
13
  this.client = axios.create({
9
14
  baseURL: config.apiUrl,
10
15
  headers: {
@@ -17,12 +22,16 @@ export class Rent2bApiClient {
17
22
  async resolveOrganization() {
18
23
  if (this.organizationId)
19
24
  return this.organizationId;
20
- const response = await this.client.get('/organizations');
21
- const orgs = response.data;
22
- if (!Array.isArray(orgs) || orgs.length === 0) {
23
- throw new Error('No organizations found for this API key');
25
+ // Resolve the organization bound to THIS API key. `GET /organizations` is
26
+ // NOT key-scoped (it returns every organization in the system, ordered with
27
+ // org 1 first), so taking [0] picked the wrong org for multi-tenant keys.
28
+ // `GET /zapier/me` is key-scoped and returns the authenticated key's org.
29
+ const response = await this.client.get('/zapier/me');
30
+ const orgId = response.data?.organization?.id ?? response.data?.id;
31
+ if (typeof orgId !== 'number') {
32
+ throw new Error('Could not resolve organization for this API key');
24
33
  }
25
- this.organizationId = orgs[0].id;
34
+ this.organizationId = orgId;
26
35
  return this.organizationId;
27
36
  }
28
37
  getOrganizationId() {
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ const mockGet = vi.fn();
3
+ const mockRequest = vi.fn();
4
+ vi.mock('axios', () => ({
5
+ default: {
6
+ create: () => ({ get: mockGet, request: mockRequest }),
7
+ },
8
+ }));
9
+ const { Rent2bApiClient } = await import('./api-client.js');
10
+ describe('resolveOrganization', () => {
11
+ beforeEach(() => {
12
+ mockGet.mockReset();
13
+ mockRequest.mockReset();
14
+ });
15
+ it('resolves the key-scoped org via /zapier/me, not GET /organizations[0]', async () => {
16
+ // /zapier/me returns the org bound to THIS api key (Haus Hana = 54).
17
+ mockGet.mockResolvedValueOnce({
18
+ data: { id: 54, organization: { id: 54, name: 'Haus Hana' } },
19
+ });
20
+ const client = new Rent2bApiClient({ apiKey: 'r2b_x', apiUrl: 'http://api' });
21
+ const org = await client.resolveOrganization();
22
+ expect(mockGet).toHaveBeenCalledWith('/zapier/me');
23
+ expect(org).toBe(54);
24
+ // must NOT fall back to the unscoped /organizations list
25
+ expect(mockGet).not.toHaveBeenCalledWith('/organizations');
26
+ });
27
+ it('caches the resolved org (one lookup)', async () => {
28
+ mockGet.mockResolvedValue({ data: { organization: { id: 54 } } });
29
+ const client = new Rent2bApiClient({ apiKey: 'r2b_x', apiUrl: 'http://api' });
30
+ await client.resolveOrganization();
31
+ await client.resolveOrganization();
32
+ expect(mockGet).toHaveBeenCalledTimes(1);
33
+ expect(client.getOrganizationId()).toBe(54);
34
+ });
35
+ it('throws when /zapier/me has no organization id', async () => {
36
+ mockGet.mockResolvedValueOnce({ data: {} });
37
+ const client = new Rent2bApiClient({ apiKey: 'r2b_x', apiUrl: 'http://api' });
38
+ await expect(client.resolveOrganization()).rejects.toThrow();
39
+ });
40
+ });
41
+ //# sourceMappingURL=api-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.js","sourceRoot":"","sources":["../src/api-client.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE9D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;AACxB,MAAM,WAAW,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;AAE5B,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IACtB,OAAO,EAAE;QACP,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;KACvD;CACF,CAAC,CAAC,CAAC;AAEJ,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAE5D,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,CAAC,SAAS,EAAE,CAAC;QACpB,WAAW,CAAC,SAAS,EAAE,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,qEAAqE;QACrE,OAAO,CAAC,qBAAqB,CAAC;YAC5B,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE;SAC9D,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9E,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC;QACnD,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrB,yDAAyD;QACzD,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,OAAO,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9E,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACnC,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,OAAO,CAAC,qBAAqB,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9E,MAAM,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ export interface AuthorizePageParams {
2
+ clientId: string;
3
+ clientName?: string;
4
+ redirectUri: string;
5
+ state: string;
6
+ codeChallenge: string;
7
+ scopes: string;
8
+ resource: string;
9
+ }
10
+ export declare function renderAuthorizePage(p: AuthorizePageParams): string;
11
+ export interface AuthorizeSubmitFields {
12
+ apiKey: string;
13
+ clientId: string;
14
+ redirectUri: string;
15
+ state: string;
16
+ codeChallenge: string;
17
+ }
18
+ export declare function parseAuthorizeSubmit(body: Record<string, unknown>): AuthorizeSubmitFields;
@@ -0,0 +1,60 @@
1
+ function esc(value) {
2
+ return value
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/"/g, '&quot;')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;');
7
+ }
8
+ function hidden(name, value) {
9
+ return `<input type="hidden" name="${esc(name)}" value="${esc(value)}">`;
10
+ }
11
+ export function renderAuthorizePage(p) {
12
+ const appName = p.clientName ? esc(p.clientName) : 'this application';
13
+ // action is relative so it resolves under the gateway path prefix (/rent2b/authorize/submit)
14
+ return `<!doctype html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="utf-8">
18
+ <meta name="viewport" content="width=device-width, initial-scale=1">
19
+ <title>Connect rent2b</title>
20
+ <style>
21
+ body { font-family: system-ui, sans-serif; max-width: 28rem; margin: 4rem auto; padding: 0 1rem; color: #1a1a1a; }
22
+ h1 { font-size: 1.25rem; } label { display:block; margin: 1rem 0 .25rem; font-weight: 600; }
23
+ input[type=text] { width: 100%; padding: .6rem; font-size: 1rem; border: 1px solid #bbb; border-radius: .4rem; }
24
+ button { margin-top: 1.25rem; padding: .6rem 1.2rem; font-size: 1rem; border: 0; border-radius: .4rem; background: #111; color: #fff; cursor: pointer; }
25
+ p.hint { color: #555; font-size: .85rem; }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <h1>Connect ${appName} to rent2b</h1>
30
+ <p class="hint">Enter your rent2b API key to grant access to your organization's data. Your key starts with <code>r2b_</code>.</p>
31
+ <form method="post" action="authorize/submit">
32
+ ${hidden('clientId', p.clientId)}
33
+ ${hidden('redirectUri', p.redirectUri)}
34
+ ${hidden('state', p.state)}
35
+ ${hidden('codeChallenge', p.codeChallenge)}
36
+ ${hidden('scopes', p.scopes)}
37
+ ${hidden('resource', p.resource)}
38
+ <label for="apiKey">rent2b API key</label>
39
+ <input id="apiKey" name="apiKey" type="text" autocomplete="off" spellcheck="false" placeholder="r2b_..." required>
40
+ <button type="submit">Authorize</button>
41
+ </form>
42
+ </body>
43
+ </html>`;
44
+ }
45
+ export function parseAuthorizeSubmit(body) {
46
+ const str = (k) => {
47
+ const v = body[k];
48
+ if (typeof v !== 'string' || v.length === 0)
49
+ throw new Error(`Missing field: ${k}`);
50
+ return v;
51
+ };
52
+ return {
53
+ apiKey: str('apiKey'),
54
+ clientId: str('clientId'),
55
+ redirectUri: str('redirectUri'),
56
+ state: typeof body.state === 'string' ? body.state : '',
57
+ codeChallenge: str('codeChallenge'),
58
+ };
59
+ }
60
+ //# sourceMappingURL=authorize-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize-page.js","sourceRoot":"","sources":["../../src/auth/authorize-page.ts"],"names":[],"mappings":"AAUA,SAAS,GAAG,CAAC,KAAa;IACxB,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,KAAa;IACzC,OAAO,8BAA8B,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,CAAsB;IACxD,MAAM,OAAO,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC;IACtE,6FAA6F;IAC7F,OAAO;;;;;;;;;;;;;;;gBAeO,OAAO;;;MAGjB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC;MAC9B,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC,WAAW,CAAC;MACpC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC;MACxB,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,aAAa,CAAC;MACxC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC;MAC1B,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC;;;;;;QAM5B,CAAC;AACT,CAAC;AAUD,MAAM,UAAU,oBAAoB,CAAC,IAA6B;IAChE,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE;QAChC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC;QACpF,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC;QACrB,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC;QACzB,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC;QAC/B,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACvD,aAAa,EAAE,GAAG,CAAC,eAAe,CAAC;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderAuthorizePage, parseAuthorizeSubmit } from './authorize-page.js';
3
+ describe('renderAuthorizePage', () => {
4
+ it('embeds params as hidden fields and escapes attributes', () => {
5
+ const html = renderAuthorizePage({
6
+ clientId: 'c1',
7
+ clientName: 'Claude',
8
+ redirectUri: 'https://claude.ai/cb',
9
+ state: 'st"ate',
10
+ codeChallenge: 'CHAL',
11
+ scopes: '',
12
+ resource: '',
13
+ });
14
+ expect(html).toContain('name="apiKey"');
15
+ expect(html).toContain('action="authorize/submit"');
16
+ expect(html).toContain('value="c1"');
17
+ expect(html).toContain('value="CHAL"');
18
+ expect(html).toContain('st&quot;ate'); // escaped quote
19
+ });
20
+ });
21
+ describe('parseAuthorizeSubmit', () => {
22
+ it('parses a valid body', () => {
23
+ const fields = parseAuthorizeSubmit({
24
+ apiKey: 'r2b_x',
25
+ clientId: 'c1',
26
+ redirectUri: 'https://claude.ai/cb',
27
+ state: 's',
28
+ codeChallenge: 'CHAL',
29
+ });
30
+ expect(fields.apiKey).toBe('r2b_x');
31
+ expect(fields.redirectUri).toBe('https://claude.ai/cb');
32
+ });
33
+ it('throws when apiKey is missing', () => {
34
+ expect(() => parseAuthorizeSubmit({ clientId: 'c1', redirectUri: 'x', codeChallenge: 'y' })).toThrow();
35
+ });
36
+ });
37
+ //# sourceMappingURL=authorize-page.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize-page.test.js","sourceRoot":"","sources":["../../src/auth/authorize-page.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,IAAI,GAAG,mBAAmB,CAAC;YAC/B,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,QAAQ;YACpB,WAAW,EAAE,sBAAsB;YACnC,KAAK,EAAE,QAAQ;YACf,aAAa,EAAE,MAAM;YACrB,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QACpD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB;IACzD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,MAAM,GAAG,oBAAoB,CAAC;YAClC,MAAM,EAAE,OAAO;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,sBAAsB;YACnC,KAAK,EAAE,GAAG;YACV,aAAa,EAAE,MAAM;SACtB,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,EAAE,CACV,oBAAoB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,CAC/E,CAAC,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { type Express, type RequestHandler } from 'express';
2
+ import type { OAuthRuntimeConfig } from './oauth-config.js';
3
+ import type { Rent2bOAuthProvider } from './provider.js';
4
+ export interface RegisterOAuthOptions {
5
+ oauthCfg: OAuthRuntimeConfig;
6
+ provider: Rent2bOAuthProvider;
7
+ validateKey: (apiKey: string) => Promise<number>;
8
+ }
9
+ /**
10
+ * Wires the OAuth 2.1 + DCR surface onto an Express app and returns the
11
+ * bearer-auth middleware to guard the MCP endpoint.
12
+ *
13
+ * Note on path-prefixed deployment: the MCP SDK serves the auth handlers at the
14
+ * origin root (`/authorize`, `/token`, `/register`) and advertises them
15
+ * origin-level, dropping any issuer path. Behind the gateway, Caddy strips the
16
+ * `/rent2b` prefix before the container sees these paths, so the handlers line
17
+ * up — but the SDK's authorization-server metadata would advertise the wrong
18
+ * (origin-level) URLs. We therefore override that one document with
19
+ * path-correct endpoint URLs. The SDK's protected-resource metadata is already
20
+ * correct (it advertises the full issuer URL), so we keep it.
21
+ */
22
+ export declare function registerOAuth(app: Express, opts: RegisterOAuthOptions): RequestHandler;
23
+ /**
24
+ * Lets existing clients that authenticate with `X-API-Key: r2b_...` (and no
25
+ * `Authorization` header) keep working: copies the key into a Bearer header so
26
+ * `requireBearerAuth` -> `verifyAccessToken`'s raw-key branch accepts it.
27
+ */
28
+ export declare const xApiKeyToBearer: RequestHandler;
@@ -0,0 +1,93 @@
1
+ import express from 'express';
2
+ import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl, } from '@modelcontextprotocol/sdk/server/auth/router.js';
3
+ import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
4
+ import { parseAuthorizeSubmit } from './authorize-page.js';
5
+ /**
6
+ * Wires the OAuth 2.1 + DCR surface onto an Express app and returns the
7
+ * bearer-auth middleware to guard the MCP endpoint.
8
+ *
9
+ * Note on path-prefixed deployment: the MCP SDK serves the auth handlers at the
10
+ * origin root (`/authorize`, `/token`, `/register`) and advertises them
11
+ * origin-level, dropping any issuer path. Behind the gateway, Caddy strips the
12
+ * `/rent2b` prefix before the container sees these paths, so the handlers line
13
+ * up — but the SDK's authorization-server metadata would advertise the wrong
14
+ * (origin-level) URLs. We therefore override that one document with
15
+ * path-correct endpoint URLs. The SDK's protected-resource metadata is already
16
+ * correct (it advertises the full issuer URL), so we keep it.
17
+ */
18
+ export function registerOAuth(app, opts) {
19
+ const { oauthCfg, provider, validateKey } = opts;
20
+ const issuerHref = oauthCfg.issuerUrl.href.replace(/\/$/, '');
21
+ app.use(express.urlencoded({ extended: false }));
22
+ // Override: path-aware authorization-server metadata (registered before the
23
+ // SDK router so it shadows the SDK's origin-level document).
24
+ const authServerMetadata = {
25
+ issuer: issuerHref,
26
+ authorization_endpoint: `${issuerHref}/authorize`,
27
+ token_endpoint: `${issuerHref}/token`,
28
+ registration_endpoint: `${issuerHref}/register`,
29
+ response_types_supported: ['code'],
30
+ grant_types_supported: ['authorization_code', 'refresh_token'],
31
+ code_challenge_methods_supported: ['S256'],
32
+ token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
33
+ scopes_supported: [],
34
+ };
35
+ app.get('/.well-known/oauth-authorization-server', (_req, res) => {
36
+ res.json(authServerMetadata);
37
+ });
38
+ app.use(mcpAuthRouter({
39
+ provider,
40
+ issuerUrl: oauthCfg.issuerUrl,
41
+ baseUrl: oauthCfg.issuerUrl,
42
+ resourceServerUrl: oauthCfg.resourceUrl,
43
+ scopesSupported: [],
44
+ }));
45
+ // The API-key entry page (provider.authorize) posts here.
46
+ app.post('/authorize/submit', async (req, res) => {
47
+ let fields;
48
+ try {
49
+ fields = parseAuthorizeSubmit(req.body);
50
+ }
51
+ catch {
52
+ res.status(400).send('Missing required fields');
53
+ return;
54
+ }
55
+ let orgId;
56
+ try {
57
+ orgId = await validateKey(fields.apiKey);
58
+ }
59
+ catch {
60
+ res.status(401).setHeader('Content-Type', 'text/html; charset=utf-8');
61
+ res.send('<p>Invalid rent2b API key. <a href="javascript:history.back()">Try again</a>.</p>');
62
+ return;
63
+ }
64
+ const code = provider.issueAuthCode({
65
+ apiKey: fields.apiKey,
66
+ orgId,
67
+ codeChallenge: fields.codeChallenge,
68
+ redirectUri: fields.redirectUri,
69
+ clientId: fields.clientId,
70
+ });
71
+ const redirect = new URL(fields.redirectUri);
72
+ redirect.searchParams.set('code', code);
73
+ if (fields.state)
74
+ redirect.searchParams.set('state', fields.state);
75
+ res.redirect(302, redirect.toString());
76
+ });
77
+ return requireBearerAuth({
78
+ verifier: provider,
79
+ resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(oauthCfg.resourceUrl),
80
+ });
81
+ }
82
+ /**
83
+ * Lets existing clients that authenticate with `X-API-Key: r2b_...` (and no
84
+ * `Authorization` header) keep working: copies the key into a Bearer header so
85
+ * `requireBearerAuth` -> `verifyAccessToken`'s raw-key branch accepts it.
86
+ */
87
+ export const xApiKeyToBearer = (req, _res, next) => {
88
+ if (!req.headers['authorization'] && typeof req.headers['x-api-key'] === 'string') {
89
+ req.headers['authorization'] = `Bearer ${req.headers['x-api-key']}`;
90
+ }
91
+ next();
92
+ };
93
+ //# sourceMappingURL=http-oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-oauth.js","sourceRoot":"","sources":["../../src/auth/http-oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export interface OAuthRuntimeConfig {
2
+ issuerUrl: URL;
3
+ resourceUrl: URL;
4
+ encryptionKey: Buffer;
5
+ }
6
+ export declare function loadOAuthConfig(env?: NodeJS.ProcessEnv): OAuthRuntimeConfig;
@@ -0,0 +1,16 @@
1
+ export function loadOAuthConfig(env = process.env) {
2
+ const issuer = env.OAUTH_ISSUER_URL;
3
+ if (!issuer)
4
+ throw new Error('OAUTH_ISSUER_URL is required to enable OAuth');
5
+ const issuerUrl = new URL(issuer);
6
+ const resourceUrl = new URL(issuerUrl.href.replace(/\/$/, '') + '/mcp');
7
+ const rawKey = env.OAUTH_ENCRYPTION_KEY;
8
+ if (!rawKey)
9
+ throw new Error('OAUTH_ENCRYPTION_KEY is required to enable OAuth');
10
+ const encryptionKey = Buffer.from(rawKey, 'base64');
11
+ if (encryptionKey.length !== 32) {
12
+ throw new Error('OAUTH_ENCRYPTION_KEY must decode (base64) to exactly 32 bytes');
13
+ }
14
+ return { issuerUrl, resourceUrl, encryptionKey };
15
+ }
16
+ //# sourceMappingURL=oauth-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-config.js","sourceRoot":"","sources":["../../src/auth/oauth-config.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,eAAe,CAAC,MAAyB,OAAO,CAAC,GAAG;IAClE,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,CAAC;IACpC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC7E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAElC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;IAExE,MAAM,MAAM,GAAG,GAAG,CAAC,oBAAoB,CAAC;IACxC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACjF,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACpD,IAAI,aAAa,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;AACnD,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { loadOAuthConfig } from './oauth-config.js';
4
+ const key = randomBytes(32).toString('base64');
5
+ describe('loadOAuthConfig', () => {
6
+ it('loads issuer, derives resource, decodes key', () => {
7
+ const cfg = loadOAuthConfig({
8
+ OAUTH_ISSUER_URL: 'https://mcp.honeyfield.at/rent2b',
9
+ OAUTH_ENCRYPTION_KEY: key,
10
+ });
11
+ expect(cfg.issuerUrl.href).toBe('https://mcp.honeyfield.at/rent2b');
12
+ expect(cfg.resourceUrl.href).toBe('https://mcp.honeyfield.at/rent2b/mcp');
13
+ expect(cfg.encryptionKey.length).toBe(32);
14
+ });
15
+ it('throws when issuer missing', () => {
16
+ expect(() => loadOAuthConfig({ OAUTH_ENCRYPTION_KEY: key })).toThrow();
17
+ });
18
+ it('throws when key is not 32 bytes', () => {
19
+ expect(() => loadOAuthConfig({
20
+ OAUTH_ISSUER_URL: 'https://x/rent2b',
21
+ OAUTH_ENCRYPTION_KEY: 'c2hvcnQ=',
22
+ })).toThrow();
23
+ });
24
+ });
25
+ //# sourceMappingURL=oauth-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-config.test.js","sourceRoot":"","sources":["../../src/auth/oauth-config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAE/C,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,eAAe,CAAC;YAC1B,gBAAgB,EAAE,kCAAkC;YACpD,oBAAoB,EAAE,GAAG;SACnB,CAAC,CAAC;QACV,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACpE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;QAC1E,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,oBAAoB,EAAE,GAAG,EAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,GAAG,EAAE,CACV,eAAe,CAAC;YACd,gBAAgB,EAAE,kBAAkB;YACpC,oBAAoB,EAAE,UAAU;SAC1B,CAAC,CACV,CAAC,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Response } from 'express';
2
+ import type { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/sdk/server/auth/provider.js';
3
+ import type { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
4
+ import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
5
+ import type { TokenCodec } from './token-crypto.js';
6
+ import type { InMemoryClientsStore, AuthCodeStore, AuthCodeRecord } from './stores.js';
7
+ export interface ProviderOptions {
8
+ codec: TokenCodec;
9
+ clientsStore: InMemoryClientsStore;
10
+ codeStore: AuthCodeStore;
11
+ validateKey: (apiKey: string) => Promise<number>;
12
+ accessTtlSec?: number;
13
+ refreshTtlSec?: number;
14
+ }
15
+ export declare class Rent2bOAuthProvider implements OAuthServerProvider {
16
+ private readonly opts;
17
+ private readonly pending;
18
+ constructor(opts: ProviderOptions);
19
+ get clientsStore(): InMemoryClientsStore;
20
+ /** Called by the /authorize/submit handler after a valid r2b_ key is entered. */
21
+ issueAuthCode(input: Omit<AuthCodeRecord, 'expiresAt'>): string;
22
+ authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
23
+ challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
24
+ exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, _codeVerifier?: string, _redirectUri?: string): Promise<OAuthTokens>;
25
+ exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, _scopes?: string[]): Promise<OAuthTokens>;
26
+ verifyAccessToken(token: string): Promise<AuthInfo>;
27
+ private mintTokens;
28
+ }
@@ -0,0 +1,127 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { TokenError } from './token-crypto.js';
3
+ import { renderAuthorizePage } from './authorize-page.js';
4
+ export class Rent2bOAuthProvider {
5
+ opts;
6
+ // Holds the code record between challengeForAuthorizationCode (peek) and
7
+ // exchangeAuthorizationCode (consume), since the code store is one-time.
8
+ pending = new Map();
9
+ constructor(opts) {
10
+ this.opts = { accessTtlSec: 3600, refreshTtlSec: 60 * 60 * 24 * 30, ...opts };
11
+ }
12
+ get clientsStore() {
13
+ return this.opts.clientsStore;
14
+ }
15
+ /** Called by the /authorize/submit handler after a valid r2b_ key is entered. */
16
+ issueAuthCode(input) {
17
+ return this.opts.codeStore.create(input);
18
+ }
19
+ async authorize(client, params, res) {
20
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
21
+ res.send(renderAuthorizePage({
22
+ clientId: client.client_id,
23
+ clientName: client.client_name,
24
+ redirectUri: params.redirectUri,
25
+ state: params.state ?? '',
26
+ codeChallenge: params.codeChallenge,
27
+ scopes: (params.scopes ?? []).join(' '),
28
+ resource: params.resource ? String(params.resource) : '',
29
+ }));
30
+ }
31
+ async challengeForAuthorizationCode(_client, authorizationCode) {
32
+ let rec = this.pending.get(authorizationCode);
33
+ if (!rec) {
34
+ const taken = this.opts.codeStore.take(authorizationCode);
35
+ if (!taken)
36
+ throw new Error('Invalid or expired authorization code');
37
+ this.pending.set(authorizationCode, taken);
38
+ rec = taken;
39
+ }
40
+ return rec.codeChallenge;
41
+ }
42
+ async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier, _redirectUri) {
43
+ let rec = this.pending.get(authorizationCode);
44
+ if (rec) {
45
+ this.pending.delete(authorizationCode);
46
+ }
47
+ else {
48
+ rec = this.opts.codeStore.take(authorizationCode);
49
+ }
50
+ if (!rec)
51
+ throw new Error('Invalid or expired authorization code');
52
+ if (rec.clientId !== client.client_id) {
53
+ throw new Error('Authorization code was issued to a different client');
54
+ }
55
+ return this.mintTokens(rec.apiKey, rec.orgId, client.client_id);
56
+ }
57
+ async exchangeRefreshToken(client, refreshToken, _scopes) {
58
+ let payload;
59
+ try {
60
+ payload = this.opts.codec.decrypt(refreshToken);
61
+ }
62
+ catch {
63
+ throw new Error('Invalid refresh token');
64
+ }
65
+ if (payload.type !== 'refresh')
66
+ throw new Error('Not a refresh token');
67
+ return this.mintTokens(payload.apiKey, payload.orgId, client.client_id);
68
+ }
69
+ async verifyAccessToken(token) {
70
+ if (token.startsWith('r2b_')) {
71
+ // Backward-compat: a raw rent2b API key used directly as the bearer.
72
+ // requireBearerAuth requires an expiry; raw keys don't expire, so we set
73
+ // a short rolling window (the key is re-validated on each new session).
74
+ const orgId = await this.opts.validateKey(token);
75
+ return {
76
+ token,
77
+ clientId: 'rent2b-direct',
78
+ scopes: [],
79
+ expiresAt: Math.floor(Date.now() / 1000) + this.opts.accessTtlSec,
80
+ extra: { apiKey: token, orgId },
81
+ };
82
+ }
83
+ let payload;
84
+ try {
85
+ payload = this.opts.codec.decrypt(token);
86
+ }
87
+ catch (err) {
88
+ if (err instanceof TokenError)
89
+ throw new Error('Invalid or expired access token');
90
+ throw err;
91
+ }
92
+ if (payload.type !== 'access')
93
+ throw new Error('Not an access token');
94
+ return {
95
+ token,
96
+ clientId: 'rent2b-oauth',
97
+ scopes: [],
98
+ expiresAt: payload.exp,
99
+ extra: { apiKey: payload.apiKey, orgId: payload.orgId },
100
+ };
101
+ }
102
+ mintTokens(apiKey, orgId, _clientId) {
103
+ const now = Math.floor(Date.now() / 1000);
104
+ const access = this.opts.codec.encrypt({
105
+ apiKey,
106
+ orgId,
107
+ type: 'access',
108
+ exp: now + this.opts.accessTtlSec,
109
+ jti: randomUUID(),
110
+ });
111
+ const refresh = this.opts.codec.encrypt({
112
+ apiKey,
113
+ orgId,
114
+ type: 'refresh',
115
+ exp: now + this.opts.refreshTtlSec,
116
+ jti: randomUUID(),
117
+ });
118
+ return {
119
+ access_token: access,
120
+ token_type: 'Bearer',
121
+ expires_in: this.opts.accessTtlSec,
122
+ refresh_token: refresh,
123
+ scope: '',
124
+ };
125
+ }
126
+ }
127
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/auth/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};