@selfhost.dev/mcp-server 0.1.0

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 (59) hide show
  1. package/README.md +382 -0
  2. package/dist/auth.d.ts +18 -0
  3. package/dist/auth.js +204 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/client.d.ts +10 -0
  6. package/dist/client.js +84 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/config.d.ts +13 -0
  9. package/dist/config.js +20 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +71 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/rate-limiter.d.ts +22 -0
  15. package/dist/rate-limiter.js +124 -0
  16. package/dist/rate-limiter.js.map +1 -0
  17. package/dist/session.d.ts +25 -0
  18. package/dist/session.js +48 -0
  19. package/dist/session.js.map +1 -0
  20. package/dist/tools/alerts.d.ts +2 -0
  21. package/dist/tools/alerts.js +372 -0
  22. package/dist/tools/alerts.js.map +1 -0
  23. package/dist/tools/auth.d.ts +2 -0
  24. package/dist/tools/auth.js +142 -0
  25. package/dist/tools/auth.js.map +1 -0
  26. package/dist/tools/backups.d.ts +2 -0
  27. package/dist/tools/backups.js +294 -0
  28. package/dist/tools/backups.js.map +1 -0
  29. package/dist/tools/credentials.d.ts +2 -0
  30. package/dist/tools/credentials.js +187 -0
  31. package/dist/tools/credentials.js.map +1 -0
  32. package/dist/tools/instances.d.ts +2 -0
  33. package/dist/tools/instances.js +555 -0
  34. package/dist/tools/instances.js.map +1 -0
  35. package/dist/tools/networking.d.ts +2 -0
  36. package/dist/tools/networking.js +240 -0
  37. package/dist/tools/networking.js.map +1 -0
  38. package/dist/tools/organizations.d.ts +2 -0
  39. package/dist/tools/organizations.js +260 -0
  40. package/dist/tools/organizations.js.map +1 -0
  41. package/dist/tools/pg-config.d.ts +2 -0
  42. package/dist/tools/pg-config.js +59 -0
  43. package/dist/tools/pg-config.js.map +1 -0
  44. package/dist/types/api.d.ts +20 -0
  45. package/dist/types/api.js +2 -0
  46. package/dist/types/api.js.map +1 -0
  47. package/dist/types/models.d.ts +44 -0
  48. package/dist/types/models.js +3 -0
  49. package/dist/types/models.js.map +1 -0
  50. package/dist/types/tiers.d.ts +15 -0
  51. package/dist/types/tiers.js +91 -0
  52. package/dist/types/tiers.js.map +1 -0
  53. package/dist/utils/formatters.d.ts +1 -0
  54. package/dist/utils/formatters.js +28 -0
  55. package/dist/utils/formatters.js.map +1 -0
  56. package/dist/utils/validators.d.ts +14 -0
  57. package/dist/utils/validators.js +9 -0
  58. package/dist/utils/validators.js.map +1 -0
  59. package/package.json +51 -0
package/dist/client.js ADDED
@@ -0,0 +1,84 @@
1
+ import { config } from "./config.js";
2
+ import { getValidToken } from "./auth.js";
3
+ import { rateLimiter } from "./rate-limiter.js";
4
+ import { session } from "./session.js";
5
+ import { stripSensitiveFields } from "./utils/formatters.js";
6
+ export async function apiRequest(path, options = {}) {
7
+ const { method = "GET", toolName, skipOrgInjection } = options;
8
+ let { body, query } = options;
9
+ // Auto-inject organization_id from session if active org is set
10
+ const orgId = session.getActiveOrgId();
11
+ if (orgId && !skipOrgInjection) {
12
+ if (method === "GET") {
13
+ if (!query?.organization_id) {
14
+ query = { ...query, organization_id: orgId };
15
+ }
16
+ }
17
+ else {
18
+ if (!body?.organization_id) {
19
+ body = { ...body, organization_id: orgId };
20
+ }
21
+ }
22
+ }
23
+ // Rate limiting
24
+ if (toolName) {
25
+ rateLimiter.acquire(toolName);
26
+ }
27
+ try {
28
+ const token = await getValidToken();
29
+ // Build URL with query params
30
+ let url = `${config.apiUrl}${path}`;
31
+ if (query) {
32
+ const params = new URLSearchParams();
33
+ for (const [key, value] of Object.entries(query)) {
34
+ if (value !== undefined)
35
+ params.set(key, value);
36
+ }
37
+ const qs = params.toString();
38
+ if (qs)
39
+ url += `?${qs}`;
40
+ }
41
+ const headers = {
42
+ Authorization: `Bearer ${token}`,
43
+ "Content-Type": "application/json",
44
+ Accept: "application/json",
45
+ };
46
+ const fetchOptions = { method, headers };
47
+ if (body && method !== "GET") {
48
+ fetchOptions.body = JSON.stringify(body);
49
+ }
50
+ const res = await fetch(url, fetchOptions);
51
+ // 204 No Content
52
+ if (res.status === 204) {
53
+ return { success: true, data: null, message: "Success", statusCode: 204 };
54
+ }
55
+ const json = await res.json();
56
+ // Rails standard response format: { status, data, message, status_code }
57
+ const isSuccess = json.status === "success" || (res.ok && json.status !== "error");
58
+ return {
59
+ success: isSuccess,
60
+ data: isSuccess ? stripSensitiveFields(json.data) : null,
61
+ message: json.message || res.statusText,
62
+ statusCode: json.status_code || res.status,
63
+ };
64
+ }
65
+ catch (error) {
66
+ // Rate limit errors are thrown before fetch — re-throw as-is
67
+ if (error instanceof Error && error.message.includes("Rate limit")) {
68
+ throw error;
69
+ }
70
+ const message = error instanceof Error ? error.message : "Unknown error";
71
+ return {
72
+ success: false,
73
+ data: null,
74
+ message,
75
+ statusCode: 0,
76
+ };
77
+ }
78
+ finally {
79
+ if (toolName) {
80
+ rateLimiter.release();
81
+ }
82
+ }
83
+ }
84
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAW7D,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,UAA0B,EAAE;IAE5B,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAC/D,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAE9B,gEAAgE;IAChE,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IACvC,IAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC/B,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC;gBAC5B,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;YAC/C,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,CAAC;gBAC3B,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,IAAI,QAAQ,EAAE,CAAC;QACb,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,aAAa,EAAE,CAAC;QAEpC,8BAA8B;QAC9B,IAAI,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACpC,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;YACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACjD,IAAI,KAAK,KAAK,SAAS;oBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAClD,CAAC;YACD,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;YAC7B,IAAI,EAAE;gBAAE,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;QAC1B,CAAC;QAED,MAAM,OAAO,GAA2B;YACtC,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;SAC3B,CAAC;QAEF,MAAM,YAAY,GAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACtD,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YAC7B,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAE3C,iBAAiB;QACjB,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QAC5E,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAE9B,yEAAyE;QACzE,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC;QAEnF,OAAO;YACL,OAAO,EAAE,SAAS;YAClB,IAAI,EAAE,SAAS,CAAC,CAAC,CAAE,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAO,CAAC,CAAC,CAAC,IAAI;YAC/D,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC,UAAU;YACvC,UAAU,EAAE,IAAI,CAAC,WAAW,IAAI,GAAG,CAAC,MAAM;SAC3C,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,6DAA6D;QAC7D,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACnE,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;QACzE,OAAO;YACL,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,IAAI;YACV,OAAO;YACP,UAAU,EAAE,CAAC;SACd,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,IAAI,QAAQ,EAAE,CAAC;YACb,WAAW,CAAC,OAAO,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import "dotenv/config";
2
+ import type { TierName } from "./types/tiers.js";
3
+ declare function log(message: string): void;
4
+ export declare const config: {
5
+ apiUrl: string;
6
+ consoleUrl: string;
7
+ tierOverride: TierName | undefined;
8
+ firebaseApiKey: string | undefined;
9
+ firebaseRefreshToken: string | undefined;
10
+ credentialsPath: string;
11
+ log: typeof log;
12
+ };
13
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,20 @@
1
+ import "dotenv/config";
2
+ function log(message) {
3
+ process.stderr.write(`[selfhost-mcp] ${message}\n`);
4
+ }
5
+ const API_URL = process.env.API_URL || "https://api.selfhost.dev";
6
+ const CONSOLE_URL = process.env.CONSOLE_URL || "https://console.selfhost.dev";
7
+ const TIER_OVERRIDE = process.env.TIER;
8
+ // Headless/CI fallback — skip browser login if these are set
9
+ const FIREBASE_API_KEY = process.env.FIREBASE_API_KEY;
10
+ const FIREBASE_REFRESH_TOKEN = process.env.FIREBASE_REFRESH_TOKEN;
11
+ export const config = {
12
+ apiUrl: API_URL,
13
+ consoleUrl: CONSOLE_URL,
14
+ tierOverride: TIER_OVERRIDE,
15
+ firebaseApiKey: FIREBASE_API_KEY,
16
+ firebaseRefreshToken: FIREBASE_REFRESH_TOKEN,
17
+ credentialsPath: `${process.env.HOME}/.selfhost/credentials.json`,
18
+ log,
19
+ };
20
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAGvB,SAAS,GAAG,CAAC,OAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,OAAO,IAAI,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,0BAA0B,CAAC;AAClE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,8BAA8B,CAAC;AAC9E,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,IAA4B,CAAC;AAE/D,6DAA6D;AAC7D,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;AACtD,MAAM,sBAAsB,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;AAElE,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,MAAM,EAAE,OAAO;IACf,UAAU,EAAE,WAAW;IACvB,YAAY,EAAE,aAAa;IAC3B,cAAc,EAAE,gBAAgB;IAChC,oBAAoB,EAAE,sBAAsB;IAC5C,eAAe,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,6BAA6B;IACjE,GAAG;CACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { config } from "./config.js";
5
+ import { forceReauth, getValidToken, hasCredentials, wasAuthPrompted, markAuthPrompted } from "./auth.js";
6
+ import { session } from "./session.js";
7
+ import { registerAuthTools } from "./tools/auth.js";
8
+ import { registerOrganizationTools } from "./tools/organizations.js";
9
+ import { registerCredentialTools } from "./tools/credentials.js";
10
+ import { registerNetworkingTools } from "./tools/networking.js";
11
+ import { registerInstanceTools } from "./tools/instances.js";
12
+ import { registerPgConfigTools } from "./tools/pg-config.js";
13
+ import { registerBackupTools } from "./tools/backups.js";
14
+ import { registerAlertTools } from "./tools/alerts.js";
15
+ const server = new McpServer({
16
+ name: "selfhost",
17
+ version: "0.1.0",
18
+ });
19
+ // Module 1: Auth & Users
20
+ registerAuthTools(server);
21
+ // Module 2: Organizations & Members
22
+ registerOrganizationTools(server);
23
+ // Module 3: Cloud Credentials
24
+ registerCredentialTools(server);
25
+ // Module 4: Networking (VPCs, Subnets, Security Groups)
26
+ registerNetworkingTools(server);
27
+ // Module 5: Instance Lifecycle
28
+ registerInstanceTools(server);
29
+ // Module 6: PostgreSQL Config
30
+ registerPgConfigTools(server);
31
+ // Module 7: Backups & Snapshots
32
+ registerBackupTools(server);
33
+ // Module 8: Alerts & Notifications
34
+ registerAlertTools(server);
35
+ // Server utility tools
36
+ server.tool("reauthenticate", "Clear stored credentials and sign in again (use to switch accounts or fix auth issues)", {}, async () => {
37
+ try {
38
+ session.clear();
39
+ await forceReauth();
40
+ return {
41
+ content: [{
42
+ type: "text",
43
+ text: "Re-authentication successful. Session cleared.\nNext: call `get_my_memberships` to see your organizations, then `select_organization` to pick one.",
44
+ }],
45
+ };
46
+ }
47
+ catch (err) {
48
+ return { content: [{ type: "text", text: `Re-authentication failed: ${err instanceof Error ? err.message : err}` }] };
49
+ }
50
+ });
51
+ async function main() {
52
+ config.log("Starting SelfHost MCP server...");
53
+ config.log(`API URL: ${config.apiUrl}`);
54
+ if (config.tierOverride) {
55
+ config.log(`Tier override: ${config.tierOverride}`);
56
+ }
57
+ const transport = new StdioServerTransport();
58
+ await server.connect(transport);
59
+ // First-time setup: if no credentials exist AND we haven't prompted before, trigger browser auth
60
+ if (!(await hasCredentials()) && !(await wasAuthPrompted())) {
61
+ markAuthPrompted().catch(() => { });
62
+ getValidToken().catch(() => {
63
+ // Timeout or error — user can retry via any tool call
64
+ });
65
+ }
66
+ }
67
+ main().catch((err) => {
68
+ config.log(`Fatal error: ${err}`);
69
+ process.exit(1);
70
+ });
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { type TierName } from "./types/tiers.js";
2
+ declare class RateLimiter {
3
+ private tier;
4
+ private isAdmin;
5
+ private readBucket;
6
+ private writeBucket;
7
+ private maxConcurrent;
8
+ private activeRequests;
9
+ private bootstrapRemaining;
10
+ constructor();
11
+ setTier(tier: TierName): void;
12
+ getTier(): TierName;
13
+ /**
14
+ * Acquire permission to make a request.
15
+ * Throws with a descriptive message if rate limited.
16
+ */
17
+ acquire(toolName: string): void;
18
+ release(): void;
19
+ getWelcomeMessage(orgName: string): string;
20
+ }
21
+ export declare const rateLimiter: RateLimiter;
22
+ export {};
@@ -0,0 +1,124 @@
1
+ import { TIER_CONFIGS, categorizeOperation } from "./types/tiers.js";
2
+ import { config } from "./config.js";
3
+ function createBucket(maxTokens, ratePerSecond) {
4
+ return {
5
+ tokens: maxTokens,
6
+ maxTokens,
7
+ ratePerSecond,
8
+ lastRefill: Date.now(),
9
+ };
10
+ }
11
+ function refillBucket(bucket) {
12
+ const now = Date.now();
13
+ const elapsed = (now - bucket.lastRefill) / 1000;
14
+ bucket.tokens = Math.min(bucket.maxTokens, bucket.tokens + elapsed * bucket.ratePerSecond);
15
+ bucket.lastRefill = now;
16
+ }
17
+ function tryConsume(bucket) {
18
+ refillBucket(bucket);
19
+ if (bucket.tokens >= 1) {
20
+ bucket.tokens -= 1;
21
+ return { ok: true, retryAfterSeconds: 0 };
22
+ }
23
+ // How long until 1 token is available?
24
+ const deficit = 1 - bucket.tokens;
25
+ const retryAfterSeconds = Math.ceil(deficit / bucket.ratePerSecond);
26
+ return { ok: false, retryAfterSeconds };
27
+ }
28
+ class RateLimiter {
29
+ tier = "FREE";
30
+ isAdmin = false;
31
+ readBucket;
32
+ writeBucket;
33
+ maxConcurrent;
34
+ activeRequests = 0;
35
+ bootstrapRemaining = 5;
36
+ constructor() {
37
+ const tierConfig = TIER_CONFIGS.FREE;
38
+ this.readBucket = createBucket(tierConfig.readBucket.burst, tierConfig.readBucket.ratePerSecond);
39
+ this.writeBucket = createBucket(tierConfig.writeBucket.burst, tierConfig.writeBucket.ratePerSecond);
40
+ this.maxConcurrent = tierConfig.maxConcurrent;
41
+ }
42
+ setTier(tier) {
43
+ this.tier = tier;
44
+ this.isAdmin = tier === "ADMIN";
45
+ if (this.isAdmin) {
46
+ config.log("Admin tier set — rate limits disabled.");
47
+ return;
48
+ }
49
+ const tierConfig = TIER_CONFIGS[tier];
50
+ this.readBucket = createBucket(tierConfig.readBucket.burst, tierConfig.readBucket.ratePerSecond);
51
+ this.writeBucket = createBucket(tierConfig.writeBucket.burst, tierConfig.writeBucket.ratePerSecond);
52
+ this.maxConcurrent = tierConfig.maxConcurrent;
53
+ config.log(`Tier set to ${tier}: ${tierConfig.readBucket.ratePerSecond * 60} reads/min, ${tierConfig.writeBucket.ratePerSecond * 60} writes/min, ${tierConfig.maxConcurrent} concurrent.`);
54
+ }
55
+ getTier() {
56
+ return this.tier;
57
+ }
58
+ /**
59
+ * Acquire permission to make a request.
60
+ * Throws with a descriptive message if rate limited.
61
+ */
62
+ acquire(toolName) {
63
+ // Admin: no limits
64
+ if (this.isAdmin) {
65
+ this.activeRequests++;
66
+ return;
67
+ }
68
+ // Env var override
69
+ if (config.tierOverride) {
70
+ this.activeRequests++;
71
+ return;
72
+ }
73
+ const category = categorizeOperation(toolName);
74
+ // Exempt tools (static data, no DB hit)
75
+ if (category === "exempt") {
76
+ return;
77
+ }
78
+ // Bootstrap phase: allow first 5 requests before tier is resolved
79
+ if (this.bootstrapRemaining > 0) {
80
+ this.bootstrapRemaining--;
81
+ this.activeRequests++;
82
+ return;
83
+ }
84
+ // Concurrency check
85
+ if (this.activeRequests >= this.maxConcurrent) {
86
+ throw new Error(`Too many concurrent requests. Your ${this.tier} plan allows ${this.maxConcurrent} simultaneous requests.\n` +
87
+ `Please wait for a current request to complete.`);
88
+ }
89
+ // Token bucket check
90
+ const bucket = category === "write" ? this.writeBucket : this.readBucket;
91
+ const result = tryConsume(bucket);
92
+ if (!result.ok) {
93
+ const bucketType = category === "write" ? "write" : "read";
94
+ const tierConfig = TIER_CONFIGS[this.tier];
95
+ const readRate = Math.round(tierConfig.readBucket.ratePerSecond * 60);
96
+ const writeRate = Math.round(tierConfig.writeBucket.ratePerSecond * 60);
97
+ throw new Error(`Rate limit reached. You've used all available ${bucketType} capacity on the ${this.tier} plan.\n` +
98
+ `You can retry in ${result.retryAfterSeconds} second(s).\n\n` +
99
+ `Current plan: ${this.tier} (${readRate} reads/min, ${writeRate} writes/min)\n` +
100
+ `To increase limits, upgrade at https://console.selfhost.dev/billing`);
101
+ }
102
+ this.activeRequests++;
103
+ }
104
+ release() {
105
+ if (this.activeRequests > 0) {
106
+ this.activeRequests--;
107
+ }
108
+ }
109
+ getWelcomeMessage(orgName) {
110
+ if (this.isAdmin) {
111
+ return `Welcome to ${orgName}. You are on the ADMIN plan.\nRate limits: unlimited. No concurrency restrictions.`;
112
+ }
113
+ const tierConfig = TIER_CONFIGS[this.tier];
114
+ const readRate = Math.round(tierConfig.readBucket.ratePerSecond * 60);
115
+ const writeRate = Math.round(tierConfig.writeBucket.ratePerSecond * 60);
116
+ const concurrent = tierConfig.maxConcurrent === Infinity ? "unlimited" : String(tierConfig.maxConcurrent);
117
+ return (`Welcome to ${orgName}. You are on the ${this.tier} plan.\n` +
118
+ `Rate limits: ${readRate} reads/min, ${writeRate} writes/min (reference data lookups are unlimited).\n` +
119
+ `Up to ${concurrent} concurrent requests.`);
120
+ }
121
+ }
122
+ // Singleton
123
+ export const rateLimiter = new RateLimiter();
124
+ //# sourceMappingURL=rate-limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ interface ActiveOrganization {
2
+ id: string;
3
+ name: string;
4
+ billingPlan: string;
5
+ }
6
+ interface SessionUser {
7
+ id: string;
8
+ email: string;
9
+ displayName: string | null;
10
+ isAdmin: boolean;
11
+ }
12
+ declare class Session {
13
+ private _user;
14
+ private _activeOrg;
15
+ setUser(user: SessionUser): void;
16
+ getUser(): SessionUser | null;
17
+ isAdmin(): boolean;
18
+ setActiveOrg(org: ActiveOrganization): void;
19
+ getActiveOrg(): ActiveOrganization | null;
20
+ getActiveOrgId(): string | null;
21
+ hasActiveOrg(): boolean;
22
+ clear(): void;
23
+ }
24
+ export declare const session: Session;
25
+ export {};
@@ -0,0 +1,48 @@
1
+ import { rateLimiter } from "./rate-limiter.js";
2
+ import { config } from "./config.js";
3
+ const BILLING_PLAN_TO_TIER = {
4
+ free: "FREE",
5
+ starter: "STARTER",
6
+ pro: "PRO",
7
+ enterprise: "ENTERPRISE",
8
+ };
9
+ class Session {
10
+ _user = null;
11
+ _activeOrg = null;
12
+ setUser(user) {
13
+ this._user = user;
14
+ if (user.isAdmin) {
15
+ rateLimiter.setTier("ADMIN");
16
+ }
17
+ }
18
+ getUser() {
19
+ return this._user;
20
+ }
21
+ isAdmin() {
22
+ return this._user?.isAdmin ?? false;
23
+ }
24
+ setActiveOrg(org) {
25
+ this._activeOrg = org;
26
+ // Admin tier always takes priority over org billing_plan
27
+ if (!this.isAdmin()) {
28
+ const tier = BILLING_PLAN_TO_TIER[org.billingPlan] ?? "FREE";
29
+ rateLimiter.setTier(tier);
30
+ }
31
+ config.log(`Active org: ${org.name} (${org.id}), billing_plan: ${org.billingPlan}`);
32
+ }
33
+ getActiveOrg() {
34
+ return this._activeOrg;
35
+ }
36
+ getActiveOrgId() {
37
+ return this._activeOrg?.id ?? null;
38
+ }
39
+ hasActiveOrg() {
40
+ return this._activeOrg !== null;
41
+ }
42
+ clear() {
43
+ this._user = null;
44
+ this._activeOrg = null;
45
+ }
46
+ }
47
+ export const session = new Session();
48
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAgBrC,MAAM,oBAAoB,GAA6B;IACrD,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,GAAG,EAAE,KAAK;IACV,UAAU,EAAE,YAAY;CACzB,CAAC;AAEF,MAAM,OAAO;IACH,KAAK,GAAuB,IAAI,CAAC;IACjC,UAAU,GAA8B,IAAI,CAAC;IAErD,OAAO,CAAC,IAAiB;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,CAAC;IACtC,CAAC;IAED,YAAY,CAAC,GAAuB;QAClC,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QAEtB,yDAAyD;QACzD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC;YAC7D,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;QAED,MAAM,CAAC,GAAG,CAAC,eAAe,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,oBAAoB,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,cAAc;QACZ,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,IAAI,CAAC;IACrC,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC;IAClC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IACzB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAlertTools(server: McpServer): void;