@nexvora/mcp-server 0.3.1 → 0.3.3
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 +15 -13
- package/dist/NexvoraClient.d.ts.map +1 -1
- package/dist/NexvoraClient.js +21 -3
- package/dist/NexvoraClient.js.map +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +26 -20
- package/dist/cli.js.map +1 -1
- package/dist/createServer.d.ts +7 -0
- package/dist/createServer.d.ts.map +1 -1
- package/dist/createServer.js +3 -3
- package/dist/createServer.js.map +1 -1
- package/package.json +6 -2
- package/CHANGELOG.md +0 -208
- package/docs/setup/chatgpt-desktop.md +0 -120
- package/docs/setup/claude-code.md +0 -152
- package/docs/setup/cursor.md +0 -129
- package/src/NexvoraClient.ts +0 -328
- package/src/RateLimiter.ts +0 -74
- package/src/__tests__/NexvoraClient.test.ts +0 -424
- package/src/__tests__/RateLimiter.test.ts +0 -151
- package/src/__tests__/auth/oauth.test.ts +0 -246
- package/src/__tests__/cache.test.ts +0 -64
- package/src/__tests__/config.test.ts +0 -98
- package/src/__tests__/defineTool.test.ts +0 -223
- package/src/__tests__/fixtures/config.json +0 -7
- package/src/__tests__/integration/agentstack.integration.test.ts +0 -259
- package/src/__tests__/integration/auth_refresh.integration.test.ts +0 -227
- package/src/__tests__/integration/consulting.integration.test.ts +0 -213
- package/src/__tests__/integration/feed.integration.test.ts +0 -200
- package/src/__tests__/integration/helpers.ts +0 -118
- package/src/__tests__/integration/knowledge.integration.test.ts +0 -194
- package/src/__tests__/integration/rate_limiting.integration.test.ts +0 -207
- package/src/__tests__/integration/submit_task.integration.test.ts +0 -120
- package/src/__tests__/integration/wallet_observatory.integration.test.ts +0 -240
- package/src/__tests__/nexvora_agentstack_answer.test.ts +0 -120
- package/src/__tests__/nexvora_agentstack_ask.test.ts +0 -140
- package/src/__tests__/nexvora_agentstack_search.test.ts +0 -188
- package/src/__tests__/nexvora_consulting_book.test.ts +0 -277
- package/src/__tests__/nexvora_consulting_search.test.ts +0 -153
- package/src/__tests__/nexvora_feed_post.test.ts +0 -147
- package/src/__tests__/nexvora_feed_react.test.ts +0 -98
- package/src/__tests__/nexvora_knowledge_search.test.ts +0 -148
- package/src/__tests__/nexvora_knowledge_subscribe.test.ts +0 -173
- package/src/__tests__/nexvora_observatory.test.ts +0 -125
- package/src/__tests__/nexvora_wallet_balance.test.ts +0 -165
- package/src/auth/oauth.ts +0 -247
- package/src/cache.ts +0 -34
- package/src/cli.ts +0 -171
- package/src/config.ts +0 -70
- package/src/createServer.ts +0 -90
- package/src/defineTool.ts +0 -120
- package/src/index.ts +0 -36
- package/src/server/sse.ts +0 -149
- package/src/tools/nexvora_agentstack_answer.ts +0 -62
- package/src/tools/nexvora_agentstack_ask.ts +0 -70
- package/src/tools/nexvora_agentstack_search.ts +0 -82
- package/src/tools/nexvora_consulting_book.ts +0 -130
- package/src/tools/nexvora_consulting_search.ts +0 -85
- package/src/tools/nexvora_feed_post.ts +0 -69
- package/src/tools/nexvora_feed_react.ts +0 -48
- package/src/tools/nexvora_knowledge_search.ts +0 -81
- package/src/tools/nexvora_knowledge_subscribe.ts +0 -90
- package/src/tools/nexvora_observatory.ts +0 -87
- package/src/tools/nexvora_submit_task.ts +0 -42
- package/src/tools/nexvora_wallet_balance.ts +0 -112
- package/tsconfig.json +0 -19
package/src/NexvoraClient.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
import { type Config, type IConfigStore } from "./config.js";
|
|
4
|
-
|
|
5
|
-
export type AuditOutcome = "success" | "error" | "rate_limited" | "unauthorized";
|
|
6
|
-
|
|
7
|
-
const AuditPayloadSchema = z.object({
|
|
8
|
-
toolName: z.string().min(1),
|
|
9
|
-
outcome: z.enum(["success", "error", "rate_limited", "unauthorized"]),
|
|
10
|
-
agentId: z.string().uuid().optional(),
|
|
11
|
-
durationMs: z.number().int().positive().optional(),
|
|
12
|
-
errorCode: z.string().optional(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export type AuditPayload = z.infer<typeof AuditPayloadSchema>;
|
|
16
|
-
|
|
17
|
-
export interface NexvoraClientOptions {
|
|
18
|
-
/** Base URL of the NexVora backend (e.g. https://api.nxvora.online) */
|
|
19
|
-
baseUrl: string;
|
|
20
|
-
/** JWT access token for the authenticated user */
|
|
21
|
-
accessToken: string;
|
|
22
|
-
/** UUID of the donor agent making tool calls (optional) */
|
|
23
|
-
agentId?: string;
|
|
24
|
-
/**
|
|
25
|
-
* Optional config store for token-refresh support.
|
|
26
|
-
* When omitted the client uses the static accessToken only (no refresh).
|
|
27
|
-
*/
|
|
28
|
-
configStore?: IConfigStore;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface RefreshTokenResponse {
|
|
32
|
-
accessToken: string;
|
|
33
|
-
refreshToken: string;
|
|
34
|
-
expiresAt: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Thrown when the refresh token itself is expired or revoked. */
|
|
38
|
-
export class SessionExpiredError extends Error {
|
|
39
|
-
constructor() {
|
|
40
|
-
super(
|
|
41
|
-
"Your NexVora session has expired. Run `nexvora login` to reconnect.",
|
|
42
|
-
);
|
|
43
|
-
this.name = "SessionExpiredError";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Thrown when a PAT (Personal Access Token) is rejected as expired or revoked.
|
|
49
|
-
*
|
|
50
|
-
* <p>PATs do not refresh automatically — when one returns 401 the user must
|
|
51
|
-
* regenerate it from the web UI. The message includes the exact URL so the
|
|
52
|
-
* MCP host can surface it directly to the user without them having to dig.</p>
|
|
53
|
-
*/
|
|
54
|
-
export class PatRevokedOrExpiredError extends Error {
|
|
55
|
-
constructor() {
|
|
56
|
-
super(
|
|
57
|
-
"Your NexVora PAT was rejected — it has been revoked or expired. " +
|
|
58
|
-
"Generate a new one at https://app.nxvora.online/app/settings/mcp-tokens " +
|
|
59
|
-
"and paste it into NEXVORA_ACCESS_TOKEN in your mcp.json.",
|
|
60
|
-
);
|
|
61
|
-
this.name = "PatRevokedOrExpiredError";
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Thrown when the backend returns 403 with type=pat-scope-missing, indicating
|
|
67
|
-
* the PAT is valid but is not authorised for the tool the user just invoked.
|
|
68
|
-
*
|
|
69
|
-
* <p>The exact missing scope is included so the MCP host can render an
|
|
70
|
-
* actionable "add tool:foo to your PAT" message.</p>
|
|
71
|
-
*/
|
|
72
|
-
export class PatScopeMissingError extends Error {
|
|
73
|
-
constructor(public readonly requiredScope: string) {
|
|
74
|
-
super(
|
|
75
|
-
`Your NexVora PAT is missing the required scope: ${requiredScope}. ` +
|
|
76
|
-
"Generate a new PAT with this scope checked at " +
|
|
77
|
-
"https://app.nxvora.online/app/settings/mcp-tokens.",
|
|
78
|
-
);
|
|
79
|
-
this.name = "PatScopeMissingError";
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Wire-format prefix that identifies a PAT (as opposed to a session JWT). */
|
|
84
|
-
const PAT_PREFIX = "nxv_pat_";
|
|
85
|
-
|
|
86
|
-
/** {@code true} if the supplied access token is a Personal Access Token. */
|
|
87
|
-
function isPat(accessToken: string): boolean {
|
|
88
|
-
return accessToken.startsWith(PAT_PREFIX);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Thin HTTP client for the NexVora backend.
|
|
93
|
-
*
|
|
94
|
-
* When constructed with a {@link IConfigStore} it automatically refreshes
|
|
95
|
-
* expired access tokens — proactively (60 s before expiry) and reactively
|
|
96
|
-
* (on a first 401). Only one refresh round-trip is made even when concurrent
|
|
97
|
-
* requests all receive a 401 at the same time.
|
|
98
|
-
*/
|
|
99
|
-
export class NexvoraClient {
|
|
100
|
-
private readonly baseUrl: string;
|
|
101
|
-
private accessToken: string;
|
|
102
|
-
readonly agentId?: string;
|
|
103
|
-
private readonly configStore?: IConfigStore;
|
|
104
|
-
|
|
105
|
-
/** Guards against concurrent refresh requests — shared across all in-flight calls. */
|
|
106
|
-
private refreshPromise: Promise<void> | null = null;
|
|
107
|
-
|
|
108
|
-
constructor(options: NexvoraClientOptions) {
|
|
109
|
-
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
110
|
-
this.accessToken = options.accessToken;
|
|
111
|
-
this.agentId = options.agentId;
|
|
112
|
-
this.configStore = options.configStore;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private authHeaders(): Record<string, string> {
|
|
116
|
-
return {
|
|
117
|
-
"Content-Type": "application/json",
|
|
118
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── Token refresh ──────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
private async ensureTokenFresh(): Promise<void> {
|
|
125
|
-
// PATs never refresh — their lifetime is exactly the expiry on the
|
|
126
|
-
// mcp_pats row. Trying to /auth/refresh with a PAT would fail and
|
|
127
|
-
// confuse the user with a misleading "session expired" error.
|
|
128
|
-
if (isPat(this.accessToken)) return;
|
|
129
|
-
if (!this.configStore) return;
|
|
130
|
-
let config: Config;
|
|
131
|
-
try {
|
|
132
|
-
config = this.configStore.read();
|
|
133
|
-
} catch {
|
|
134
|
-
return; // no config file yet — continue with the static token
|
|
135
|
-
}
|
|
136
|
-
const nowSecs = Math.floor(Date.now() / 1000);
|
|
137
|
-
if (config.expiresAt < nowSecs + 60) {
|
|
138
|
-
await this.triggerRefresh(config);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Ensures only one refresh is in-flight at a time.
|
|
144
|
-
* Concurrent callers await the same promise.
|
|
145
|
-
*/
|
|
146
|
-
private triggerRefresh(config?: Config): Promise<void> {
|
|
147
|
-
if (!this.refreshPromise) {
|
|
148
|
-
this.refreshPromise = this.performRefresh(config).finally(() => {
|
|
149
|
-
this.refreshPromise = null;
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return this.refreshPromise;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async performRefresh(existingConfig?: Config): Promise<void> {
|
|
156
|
-
if (!this.configStore) return;
|
|
157
|
-
|
|
158
|
-
let config: Config;
|
|
159
|
-
try {
|
|
160
|
-
config = existingConfig ?? this.configStore.read();
|
|
161
|
-
} catch {
|
|
162
|
-
throw new SessionExpiredError();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
|
|
166
|
-
method: "POST",
|
|
167
|
-
headers: { "Content-Type": "application/json" },
|
|
168
|
-
body: JSON.stringify({ refreshToken: config.refreshToken }),
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (!response.ok) {
|
|
172
|
-
throw new SessionExpiredError();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const { accessToken, refreshToken, expiresAt } =
|
|
176
|
-
(await response.json()) as RefreshTokenResponse;
|
|
177
|
-
|
|
178
|
-
this.accessToken = accessToken;
|
|
179
|
-
this.configStore.write({ ...config, accessToken, refreshToken, expiresAt });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ── Core request dispatcher ────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
private async dispatchFetch(url: string, init: RequestInit): Promise<Response> {
|
|
185
|
-
await this.ensureTokenFresh();
|
|
186
|
-
|
|
187
|
-
let response = await fetch(url, { ...init, headers: this.authHeaders() });
|
|
188
|
-
|
|
189
|
-
if (response.status === 401) {
|
|
190
|
-
// PATs never refresh — a 401 means the token is revoked or expired.
|
|
191
|
-
// Surface a targeted error pointing at the regen URL rather than the
|
|
192
|
-
// generic "session expired, run nexvora login" message that suits
|
|
193
|
-
// JWT users.
|
|
194
|
-
if (isPat(this.accessToken)) {
|
|
195
|
-
throw new PatRevokedOrExpiredError();
|
|
196
|
-
}
|
|
197
|
-
// JWT path: try one refresh then retry the original request once.
|
|
198
|
-
if (this.configStore) {
|
|
199
|
-
await this.triggerRefresh();
|
|
200
|
-
response = await fetch(url, { ...init, headers: this.authHeaders() });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 403 with type=pat-scope-missing carries the exact scope the PAT lacks.
|
|
205
|
-
// Pull it out so we can throw a typed error that downstream tools can
|
|
206
|
-
// render as "your PAT needs tool:foo — regenerate at <url>".
|
|
207
|
-
if (response.status === 403 && isPat(this.accessToken)) {
|
|
208
|
-
const scope = await readMissingScope(response);
|
|
209
|
-
if (scope != null) {
|
|
210
|
-
throw new PatScopeMissingError(scope);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return response;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ── Public API ─────────────────────────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Sends an audit event to {@code POST /mcp/audit} fire-and-forget.
|
|
221
|
-
* Errors are silently swallowed to prevent audit failures from affecting tool callers.
|
|
222
|
-
*/
|
|
223
|
-
async sendAudit(payload: AuditPayload): Promise<void> {
|
|
224
|
-
try {
|
|
225
|
-
const validated = AuditPayloadSchema.parse(payload);
|
|
226
|
-
await fetch(`${this.baseUrl}/mcp/audit`, {
|
|
227
|
-
method: "POST",
|
|
228
|
-
headers: this.authHeaders(),
|
|
229
|
-
body: JSON.stringify(validated),
|
|
230
|
-
});
|
|
231
|
-
} catch {
|
|
232
|
-
// audit failures must not surface to the tool caller
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Makes an authenticated POST request to the NexVora backend.
|
|
238
|
-
*
|
|
239
|
-
* @throws {NexvoraApiError} on non-2xx responses
|
|
240
|
-
* @throws {SessionExpiredError} when the refresh token is also expired
|
|
241
|
-
*/
|
|
242
|
-
async post<T>(path: string, body: unknown): Promise<T> {
|
|
243
|
-
const response = await this.dispatchFetch(`${this.baseUrl}${path}`, {
|
|
244
|
-
method: "POST",
|
|
245
|
-
body: JSON.stringify(body),
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
if (!response.ok) {
|
|
249
|
-
const text = await response.text().catch(() => "");
|
|
250
|
-
throw new NexvoraApiError(response.status, text, path);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return response.json() as Promise<T>;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Makes an authenticated GET request to the NexVora backend.
|
|
258
|
-
*
|
|
259
|
-
* @throws {NexvoraApiError} on non-2xx responses
|
|
260
|
-
* @throws {SessionExpiredError} when the refresh token is also expired
|
|
261
|
-
*/
|
|
262
|
-
async get<T>(path: string): Promise<T> {
|
|
263
|
-
const response = await this.dispatchFetch(`${this.baseUrl}${path}`, {});
|
|
264
|
-
|
|
265
|
-
if (!response.ok) {
|
|
266
|
-
const text = await response.text().catch(() => "");
|
|
267
|
-
throw new NexvoraApiError(response.status, text, path);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return response.json() as Promise<T>;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Reads a 403 response body as RFC 7807 ProblemDetail and pulls the
|
|
276
|
-
* {@code required_scope} property out. Returns {@code null} for any other
|
|
277
|
-
* shape — the caller falls back to the generic error path.
|
|
278
|
-
*
|
|
279
|
-
* <p>The response body is consumed by this call; the dispatcher must not
|
|
280
|
-
* attempt to read it again. We clone first so the original {@link Response}
|
|
281
|
-
* remains usable if no scope is found.</p>
|
|
282
|
-
*/
|
|
283
|
-
async function readMissingScope(response: Response): Promise<string | null> {
|
|
284
|
-
try {
|
|
285
|
-
const cloned = response.clone();
|
|
286
|
-
const body = (await cloned.json()) as {
|
|
287
|
-
type?: string;
|
|
288
|
-
required_scope?: string;
|
|
289
|
-
};
|
|
290
|
-
if (
|
|
291
|
-
typeof body?.required_scope === "string" &&
|
|
292
|
-
body.type?.includes("pat-scope-missing")
|
|
293
|
-
) {
|
|
294
|
-
return body.required_scope;
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
// not JSON, or fetch couldn't be cloned — fall through
|
|
298
|
-
}
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Represents a non-2xx response from the NexVora API.
|
|
304
|
-
*/
|
|
305
|
-
export class NexvoraApiError extends Error {
|
|
306
|
-
constructor(
|
|
307
|
-
public readonly statusCode: number,
|
|
308
|
-
public readonly body: string,
|
|
309
|
-
public readonly path: string,
|
|
310
|
-
) {
|
|
311
|
-
super(`NexVora API error ${statusCode} on ${path}: ${body}`);
|
|
312
|
-
this.name = "NexvoraApiError";
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
get isRateLimited(): boolean {
|
|
316
|
-
return this.statusCode === 429;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
get isUnauthorized(): boolean {
|
|
320
|
-
return this.statusCode === 401 || this.statusCode === 403;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
toAuditOutcome(): AuditOutcome {
|
|
324
|
-
if (this.isRateLimited) return "rate_limited";
|
|
325
|
-
if (this.isUnauthorized) return "unauthorized";
|
|
326
|
-
return "error";
|
|
327
|
-
}
|
|
328
|
-
}
|
package/src/RateLimiter.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_RATE_LIMITS: Record<string, number> = {
|
|
2
|
-
nexvora_wallet_balance: 60,
|
|
3
|
-
nexvora_observatory: 60,
|
|
4
|
-
nexvora_agentstack_search: 30,
|
|
5
|
-
nexvora_agentstack_ask: 5,
|
|
6
|
-
nexvora_agentstack_answer: 10,
|
|
7
|
-
nexvora_feed_post: 10,
|
|
8
|
-
nexvora_feed_react: 60,
|
|
9
|
-
nexvora_consulting_search: 30,
|
|
10
|
-
nexvora_consulting_book: 5,
|
|
11
|
-
nexvora_knowledge_search: 30,
|
|
12
|
-
nexvora_knowledge_subscribe: 5,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type ConsumeResult = { allowed: true } | { allowed: false; retryAfterMs: number };
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Smooth token-bucket with continuous refill.
|
|
19
|
-
*
|
|
20
|
-
* Capacity = perMinuteLimit (burst up to the full window's worth of tokens).
|
|
21
|
-
* Refill rate = perMinuteLimit / 60 tokens-per-second (smooth, not batch).
|
|
22
|
-
*/
|
|
23
|
-
export class TokenBucket {
|
|
24
|
-
private tokens: number;
|
|
25
|
-
private lastRefill: number;
|
|
26
|
-
|
|
27
|
-
constructor(
|
|
28
|
-
private readonly capacity: number,
|
|
29
|
-
private readonly refillRatePerSec: number,
|
|
30
|
-
) {
|
|
31
|
-
this.tokens = capacity;
|
|
32
|
-
this.lastRefill = Date.now();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
tryConsume(): ConsumeResult {
|
|
36
|
-
this.refill();
|
|
37
|
-
if (this.tokens >= 1) {
|
|
38
|
-
this.tokens -= 1;
|
|
39
|
-
return { allowed: true };
|
|
40
|
-
}
|
|
41
|
-
// milliseconds until 1 token is available
|
|
42
|
-
const retryAfterMs = Math.ceil(((1 - this.tokens) / this.refillRatePerSec) * 1000);
|
|
43
|
-
return { allowed: false, retryAfterMs };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private refill(): void {
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
const elapsedSec = (now - this.lastRefill) / 1000;
|
|
49
|
-
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillRatePerSec);
|
|
50
|
-
this.lastRefill = now;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* One TokenBucket per registered tool name.
|
|
56
|
-
* Tools not in the registry are always allowed.
|
|
57
|
-
*/
|
|
58
|
-
export class RateLimiterRegistry {
|
|
59
|
-
private readonly buckets = new Map<string, TokenBucket>();
|
|
60
|
-
|
|
61
|
-
constructor(limits: Record<string, number> = DEFAULT_RATE_LIMITS) {
|
|
62
|
-
for (const [toolName, perMinute] of Object.entries(limits)) {
|
|
63
|
-
if (perMinute > 0) {
|
|
64
|
-
this.buckets.set(toolName, new TokenBucket(perMinute, perMinute / 60));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
tryConsume(toolName: string): ConsumeResult {
|
|
70
|
-
const bucket = this.buckets.get(toolName);
|
|
71
|
-
if (!bucket) return { allowed: true };
|
|
72
|
-
return bucket.tryConsume();
|
|
73
|
-
}
|
|
74
|
-
}
|