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