@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.
Files changed (66) hide show
  1. package/README.md +15 -13
  2. package/dist/NexvoraClient.d.ts.map +1 -1
  3. package/dist/NexvoraClient.js +21 -3
  4. package/dist/NexvoraClient.js.map +1 -1
  5. package/dist/cli.d.ts +2 -2
  6. package/dist/cli.js +26 -20
  7. package/dist/cli.js.map +1 -1
  8. package/dist/createServer.d.ts +7 -0
  9. package/dist/createServer.d.ts.map +1 -1
  10. package/dist/createServer.js +3 -3
  11. package/dist/createServer.js.map +1 -1
  12. package/package.json +6 -2
  13. package/CHANGELOG.md +0 -208
  14. package/docs/setup/chatgpt-desktop.md +0 -120
  15. package/docs/setup/claude-code.md +0 -152
  16. package/docs/setup/cursor.md +0 -129
  17. package/src/NexvoraClient.ts +0 -328
  18. package/src/RateLimiter.ts +0 -74
  19. package/src/__tests__/NexvoraClient.test.ts +0 -424
  20. package/src/__tests__/RateLimiter.test.ts +0 -151
  21. package/src/__tests__/auth/oauth.test.ts +0 -246
  22. package/src/__tests__/cache.test.ts +0 -64
  23. package/src/__tests__/config.test.ts +0 -98
  24. package/src/__tests__/defineTool.test.ts +0 -223
  25. package/src/__tests__/fixtures/config.json +0 -7
  26. package/src/__tests__/integration/agentstack.integration.test.ts +0 -259
  27. package/src/__tests__/integration/auth_refresh.integration.test.ts +0 -227
  28. package/src/__tests__/integration/consulting.integration.test.ts +0 -213
  29. package/src/__tests__/integration/feed.integration.test.ts +0 -200
  30. package/src/__tests__/integration/helpers.ts +0 -118
  31. package/src/__tests__/integration/knowledge.integration.test.ts +0 -194
  32. package/src/__tests__/integration/rate_limiting.integration.test.ts +0 -207
  33. package/src/__tests__/integration/submit_task.integration.test.ts +0 -120
  34. package/src/__tests__/integration/wallet_observatory.integration.test.ts +0 -240
  35. package/src/__tests__/nexvora_agentstack_answer.test.ts +0 -120
  36. package/src/__tests__/nexvora_agentstack_ask.test.ts +0 -140
  37. package/src/__tests__/nexvora_agentstack_search.test.ts +0 -188
  38. package/src/__tests__/nexvora_consulting_book.test.ts +0 -277
  39. package/src/__tests__/nexvora_consulting_search.test.ts +0 -153
  40. package/src/__tests__/nexvora_feed_post.test.ts +0 -147
  41. package/src/__tests__/nexvora_feed_react.test.ts +0 -98
  42. package/src/__tests__/nexvora_knowledge_search.test.ts +0 -148
  43. package/src/__tests__/nexvora_knowledge_subscribe.test.ts +0 -173
  44. package/src/__tests__/nexvora_observatory.test.ts +0 -125
  45. package/src/__tests__/nexvora_wallet_balance.test.ts +0 -165
  46. package/src/auth/oauth.ts +0 -247
  47. package/src/cache.ts +0 -34
  48. package/src/cli.ts +0 -171
  49. package/src/config.ts +0 -70
  50. package/src/createServer.ts +0 -90
  51. package/src/defineTool.ts +0 -120
  52. package/src/index.ts +0 -36
  53. package/src/server/sse.ts +0 -149
  54. package/src/tools/nexvora_agentstack_answer.ts +0 -62
  55. package/src/tools/nexvora_agentstack_ask.ts +0 -70
  56. package/src/tools/nexvora_agentstack_search.ts +0 -82
  57. package/src/tools/nexvora_consulting_book.ts +0 -130
  58. package/src/tools/nexvora_consulting_search.ts +0 -85
  59. package/src/tools/nexvora_feed_post.ts +0 -69
  60. package/src/tools/nexvora_feed_react.ts +0 -48
  61. package/src/tools/nexvora_knowledge_search.ts +0 -81
  62. package/src/tools/nexvora_knowledge_subscribe.ts +0 -90
  63. package/src/tools/nexvora_observatory.ts +0 -87
  64. package/src/tools/nexvora_submit_task.ts +0 -42
  65. package/src/tools/nexvora_wallet_balance.ts +0 -112
  66. package/tsconfig.json +0 -19
@@ -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
- }
@@ -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
- }