@nexvora/mcp-server 0.3.2 → 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.js +17 -11
- 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 +5 -1
- 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/cache.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal in-process TTL cache.
|
|
3
|
-
*
|
|
4
|
-
* Avoids hammering the backend when an LLM repeatedly calls the same read-only tool
|
|
5
|
-
* within a short window. TTL is per-key; expired entries are evicted lazily on access.
|
|
6
|
-
*/
|
|
7
|
-
export class TtlCache<V> {
|
|
8
|
-
private readonly store = new Map<string, { value: V; expiresAt: number }>();
|
|
9
|
-
|
|
10
|
-
constructor(private readonly ttlMs: number) {}
|
|
11
|
-
|
|
12
|
-
get(key: string): V | undefined {
|
|
13
|
-
const entry = this.store.get(key);
|
|
14
|
-
if (!entry) return undefined;
|
|
15
|
-
if (Date.now() > entry.expiresAt) {
|
|
16
|
-
this.store.delete(key);
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
return entry.value;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
set(key: string, value: V): void {
|
|
23
|
-
this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Returns the cached value if fresh, otherwise calls {@code fn} and caches the result. */
|
|
27
|
-
async getOrFetch(key: string, fn: () => Promise<V>): Promise<V> {
|
|
28
|
-
const cached = this.get(key);
|
|
29
|
-
if (cached !== undefined) return cached;
|
|
30
|
-
const fresh = await fn();
|
|
31
|
-
this.set(key, fresh);
|
|
32
|
-
return fresh;
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* nexvora CLI
|
|
4
|
-
*
|
|
5
|
-
* Commands:
|
|
6
|
-
* nexvora-mcp login [--server-url URL] — OAuth Device Grant; stores tokens
|
|
7
|
-
* nexvora-mcp serve [--transport stdio|sse] — Start the MCP server
|
|
8
|
-
* [--port PORT] — SSE listen port (default 7700)
|
|
9
|
-
* [--server-url URL] — NexVora API base URL
|
|
10
|
-
*
|
|
11
|
-
* When run with no arguments (or as a bin via "npx @nexvora/mcp-server") the
|
|
12
|
-
* default behaviour is "serve --transport stdio", preserving backward compat
|
|
13
|
-
* with existing Claude Code / Cursor configurations.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { ConfigManager } from "./config.js";
|
|
17
|
-
import { loginInteractive } from "./auth/oauth.js";
|
|
18
|
-
|
|
19
|
-
// ── Argument parsing ──────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
|
|
23
|
-
function flag(name: string): string | undefined {
|
|
24
|
-
const idx = args.indexOf(name);
|
|
25
|
-
return idx >= 0 ? args[idx + 1] : undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function hasFlag(name: string): boolean {
|
|
29
|
-
return args.includes(name);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const command = args[0] ?? "serve";
|
|
33
|
-
|
|
34
|
-
// ── Command dispatch ──────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
switch (command) {
|
|
37
|
-
case "login":
|
|
38
|
-
await runLogin();
|
|
39
|
-
break;
|
|
40
|
-
|
|
41
|
-
case "serve":
|
|
42
|
-
case "--transport": // allow "nexvora-mcp --transport sse" as shorthand
|
|
43
|
-
await runServe();
|
|
44
|
-
break;
|
|
45
|
-
|
|
46
|
-
case "--help":
|
|
47
|
-
case "-h":
|
|
48
|
-
case "help":
|
|
49
|
-
printHelp();
|
|
50
|
-
break;
|
|
51
|
-
|
|
52
|
-
default:
|
|
53
|
-
// Unknown first arg — treat the whole argv as a serve invocation
|
|
54
|
-
// (handles "nexvora" called with no subcommand from Claude Code)
|
|
55
|
-
await runServe();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ── login ─────────────────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
async function runLogin(): Promise<void> {
|
|
61
|
-
const serverUrl = flag("--server-url") ?? process.env["NEXVORA_BASE_URL"] ?? "https://api.nxvora.online";
|
|
62
|
-
|
|
63
|
-
console.error(`Authenticating with ${serverUrl} …`);
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const config = await loginInteractive(serverUrl);
|
|
67
|
-
const configManager = new ConfigManager();
|
|
68
|
-
configManager.write(config);
|
|
69
|
-
console.error(`\n✓ Logged in. Credentials stored in ~/.agentverse/config.json`);
|
|
70
|
-
console.error(` User ID : ${config.userId || "(unknown)"}`);
|
|
71
|
-
console.error(` Expires : ${new Date(config.expiresAt * 1000).toLocaleString()}`);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error(`\n✗ Login failed: ${(err as Error).message}`);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── serve ─────────────────────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
async function runServe(): Promise<void> {
|
|
81
|
-
const transport = flag("--transport") ?? "stdio";
|
|
82
|
-
const port = parseInt(flag("--port") ?? "7700", 10);
|
|
83
|
-
const serverUrl = flag("--server-url") ?? process.env["NEXVORA_BASE_URL"] ?? "https://api.nxvora.online";
|
|
84
|
-
|
|
85
|
-
if (transport === "sse") {
|
|
86
|
-
await serveSSE(serverUrl, port);
|
|
87
|
-
} else {
|
|
88
|
-
await serveStdio(serverUrl);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function serveStdio(serverUrl: string): Promise<void> {
|
|
93
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
94
|
-
const { createServer } = await import("./createServer.js");
|
|
95
|
-
|
|
96
|
-
const accessToken = resolveToken();
|
|
97
|
-
const server = await createServer({ baseUrl: serverUrl, accessToken });
|
|
98
|
-
const transport = new StdioServerTransport();
|
|
99
|
-
await server.connect(transport);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function serveSSE(serverUrl: string, port: number): Promise<void> {
|
|
103
|
-
const { startSseServer } = await import("./server/sse.js");
|
|
104
|
-
const { createServer } = await import("./createServer.js");
|
|
105
|
-
const { ConfigManager } = await import("./config.js");
|
|
106
|
-
|
|
107
|
-
const accessToken = resolveToken();
|
|
108
|
-
const mcpServer = await createServer({ baseUrl: serverUrl, accessToken });
|
|
109
|
-
|
|
110
|
-
// Token validation: accept the token from config store or the env-var token
|
|
111
|
-
const configManager = new ConfigManager();
|
|
112
|
-
startSseServer({
|
|
113
|
-
port,
|
|
114
|
-
mcpServer,
|
|
115
|
-
nexvoraBaseUrl: serverUrl,
|
|
116
|
-
validateToken: async (bearerToken: string) => {
|
|
117
|
-
if (bearerToken === accessToken) return true;
|
|
118
|
-
try {
|
|
119
|
-
const config = configManager.read();
|
|
120
|
-
return config.accessToken === bearerToken;
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Resolve the access token in priority order:
|
|
132
|
-
* 1. NEXVORA_ACCESS_TOKEN env var (CI / Docker)
|
|
133
|
-
* 2. Token stored by `nexvora-mcp login` in ~/.agentverse/config.json
|
|
134
|
-
*/
|
|
135
|
-
function resolveToken(): string {
|
|
136
|
-
const envToken = process.env["NEXVORA_ACCESS_TOKEN"];
|
|
137
|
-
if (envToken) return envToken;
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const config = new ConfigManager().read();
|
|
141
|
-
return config.accessToken;
|
|
142
|
-
} catch {
|
|
143
|
-
console.error(
|
|
144
|
-
"Error: no access token found.\n" +
|
|
145
|
-
" • Run `nexvora-mcp login` to authenticate, or\n" +
|
|
146
|
-
" • Set the NEXVORA_ACCESS_TOKEN environment variable.",
|
|
147
|
-
);
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function printHelp(): void {
|
|
153
|
-
console.log(`
|
|
154
|
-
nexvora — NexVora MCP server CLI
|
|
155
|
-
|
|
156
|
-
Usage:
|
|
157
|
-
nexvora-mcp login [--server-url URL] Authenticate via OAuth (stores token)
|
|
158
|
-
nexvora-mcp serve [--transport stdio|sse] Start the MCP server (default: stdio)
|
|
159
|
-
[--port PORT] SSE listen port (default: 7700)
|
|
160
|
-
[--server-url URL] NexVora API base URL
|
|
161
|
-
|
|
162
|
-
Environment variables:
|
|
163
|
-
NEXVORA_ACCESS_TOKEN Raw API token (bypasses stored credentials)
|
|
164
|
-
NEXVORA_BASE_URL Override the default API base URL
|
|
165
|
-
|
|
166
|
-
Examples:
|
|
167
|
-
nexvora-mcp login
|
|
168
|
-
nexvora-mcp serve --transport sse --port 7700
|
|
169
|
-
NEXVORA_ACCESS_TOKEN=tok nexvora-mcp serve --transport stdio
|
|
170
|
-
`);
|
|
171
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
|
|
5
|
-
export interface Config {
|
|
6
|
-
accessToken: string;
|
|
7
|
-
refreshToken: string;
|
|
8
|
-
/** Unix epoch seconds */
|
|
9
|
-
expiresAt: number;
|
|
10
|
-
serverUrl: string;
|
|
11
|
-
userId: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Abstraction over the config store so tools and tests can inject
|
|
16
|
-
* an in-memory implementation without touching the filesystem.
|
|
17
|
-
*/
|
|
18
|
-
export interface IConfigStore {
|
|
19
|
-
read(): Config;
|
|
20
|
-
write(config: Config): void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Default path: ~/.agentverse/config.json */
|
|
24
|
-
export function defaultConfigPath(): string {
|
|
25
|
-
return path.join(os.homedir(), ".agentverse", "config.json");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Reads and writes ~/.agentverse/config.json.
|
|
30
|
-
*
|
|
31
|
-
* Writes are atomic on POSIX (write to .tmp, then rename).
|
|
32
|
-
* On Windows the rename is not guaranteed atomic but is still safer than
|
|
33
|
-
* a direct overwrite. File permissions are set to 0600 on POSIX after write.
|
|
34
|
-
*/
|
|
35
|
-
export class ConfigManager implements IConfigStore {
|
|
36
|
-
constructor(private readonly configPath: string = defaultConfigPath()) {}
|
|
37
|
-
|
|
38
|
-
read(): Config {
|
|
39
|
-
const raw = fs.readFileSync(this.configPath, "utf8");
|
|
40
|
-
return JSON.parse(raw) as Config;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
write(config: Config): void {
|
|
44
|
-
const dir = path.dirname(this.configPath);
|
|
45
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
-
|
|
47
|
-
const content = JSON.stringify(config, null, 2);
|
|
48
|
-
const tmpPath = `${this.configPath}.tmp`;
|
|
49
|
-
|
|
50
|
-
fs.writeFileSync(tmpPath, content, { encoding: "utf8", mode: 0o600 });
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
fs.renameSync(tmpPath, this.configPath);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
// Windows: EEXIST when target exists (not atomic but acceptably safe)
|
|
56
|
-
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
57
|
-
fs.rmSync(this.configPath, { force: true });
|
|
58
|
-
fs.renameSync(tmpPath, this.configPath);
|
|
59
|
-
} else {
|
|
60
|
-
fs.rmSync(tmpPath, { force: true });
|
|
61
|
-
throw err;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Restrict permissions on POSIX (no-op on Windows)
|
|
66
|
-
if (process.platform !== "win32") {
|
|
67
|
-
fs.chmodSync(this.configPath, 0o600);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
package/src/createServer.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Factory that builds and returns a fully-configured McpServer with all 12
|
|
3
|
-
* NexVora tools registered.
|
|
4
|
-
*
|
|
5
|
-
* Extracted from index.ts so it can be shared by both the stdio entry point
|
|
6
|
-
* and the SSE HTTP server without duplication.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
|
|
12
|
-
import { defineTool, type ToolDefinition } from "./defineTool.js";
|
|
13
|
-
import { NexvoraClient } from "./NexvoraClient.js";
|
|
14
|
-
import { DEFAULT_RATE_LIMITS, RateLimiterRegistry } from "./RateLimiter.js";
|
|
15
|
-
import { nexvora_agentstack_answer } from "./tools/nexvora_agentstack_answer.js";
|
|
16
|
-
import { nexvora_agentstack_ask } from "./tools/nexvora_agentstack_ask.js";
|
|
17
|
-
import { nexvora_agentstack_search } from "./tools/nexvora_agentstack_search.js";
|
|
18
|
-
import { nexvora_consulting_book } from "./tools/nexvora_consulting_book.js";
|
|
19
|
-
import { nexvora_consulting_search } from "./tools/nexvora_consulting_search.js";
|
|
20
|
-
import { nexvora_knowledge_search } from "./tools/nexvora_knowledge_search.js";
|
|
21
|
-
import { nexvora_knowledge_subscribe } from "./tools/nexvora_knowledge_subscribe.js";
|
|
22
|
-
import { nexvora_feed_post } from "./tools/nexvora_feed_post.js";
|
|
23
|
-
import { nexvora_feed_react } from "./tools/nexvora_feed_react.js";
|
|
24
|
-
import { nexvora_observatory } from "./tools/nexvora_observatory.js";
|
|
25
|
-
import { nexvora_submit_task } from "./tools/nexvora_submit_task.js";
|
|
26
|
-
import { nexvora_wallet_balance } from "./tools/nexvora_wallet_balance.js";
|
|
27
|
-
|
|
28
|
-
const ALL_TOOLS = [
|
|
29
|
-
nexvora_submit_task,
|
|
30
|
-
nexvora_wallet_balance,
|
|
31
|
-
nexvora_observatory,
|
|
32
|
-
nexvora_agentstack_search,
|
|
33
|
-
nexvora_agentstack_ask,
|
|
34
|
-
nexvora_agentstack_answer,
|
|
35
|
-
nexvora_feed_post,
|
|
36
|
-
nexvora_feed_react,
|
|
37
|
-
nexvora_consulting_search,
|
|
38
|
-
nexvora_consulting_book,
|
|
39
|
-
nexvora_knowledge_search,
|
|
40
|
-
nexvora_knowledge_subscribe,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
export interface CreateServerOptions {
|
|
44
|
-
baseUrl: string;
|
|
45
|
-
accessToken: string;
|
|
46
|
-
agentId?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Build a McpServer with all NexVora tools registered.
|
|
51
|
-
* Fetches per-tool rate limits from the backend; silently falls back to
|
|
52
|
-
* baked-in defaults if the network is unavailable.
|
|
53
|
-
*/
|
|
54
|
-
export async function createServer(opts: CreateServerOptions): Promise<McpServer> {
|
|
55
|
-
const { baseUrl, accessToken, agentId } = opts;
|
|
56
|
-
|
|
57
|
-
const client = new NexvoraClient({ baseUrl, accessToken, agentId });
|
|
58
|
-
|
|
59
|
-
let rateLimitMap: Record<string, number> = DEFAULT_RATE_LIMITS;
|
|
60
|
-
try {
|
|
61
|
-
const fetched = await client.get<Record<string, number>>("/mcp/rate-limits");
|
|
62
|
-
rateLimitMap = { ...DEFAULT_RATE_LIMITS, ...fetched };
|
|
63
|
-
} catch {
|
|
64
|
-
// network failure or auth error — silently use defaults
|
|
65
|
-
}
|
|
66
|
-
const rateLimiter = new RateLimiterRegistry(rateLimitMap);
|
|
67
|
-
|
|
68
|
-
const server = new McpServer({
|
|
69
|
-
name: "@nexvora/mcp-server",
|
|
70
|
-
version: "0.3.1",
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
for (const tool of ALL_TOOLS) {
|
|
74
|
-
const wrappedHandler = defineTool(
|
|
75
|
-
tool as unknown as ToolDefinition<z.ZodTypeAny, unknown>,
|
|
76
|
-
client,
|
|
77
|
-
rateLimiter,
|
|
78
|
-
);
|
|
79
|
-
server.tool(
|
|
80
|
-
tool.name,
|
|
81
|
-
tool.description,
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
-
tool.inputSchema.shape as any,
|
|
84
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
-
wrappedHandler as any,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return server;
|
|
90
|
-
}
|
package/src/defineTool.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
import { NexvoraApiError, NexvoraClient } from "./NexvoraClient.js";
|
|
4
|
-
import { type RateLimiterRegistry } from "./RateLimiter.js";
|
|
5
|
-
|
|
6
|
-
export interface ToolDefinition<TInput extends z.ZodTypeAny, TOutput> {
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
inputSchema: TInput;
|
|
10
|
-
handler: (input: z.infer<TInput>, client: NexvoraClient) => Promise<TOutput>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ToolCallResult {
|
|
14
|
-
content: Array<{ type: "text"; text: string }>;
|
|
15
|
-
isError?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Wraps a raw tool handler with:
|
|
20
|
-
* - Zod input validation
|
|
21
|
-
* - Timing measurement
|
|
22
|
-
* - Fire-and-forget audit to POST /mcp/audit
|
|
23
|
-
* - Structured error handling with MCP-compatible result shape
|
|
24
|
-
*/
|
|
25
|
-
export function defineTool<TInput extends z.ZodTypeAny, TOutput>(
|
|
26
|
-
definition: ToolDefinition<TInput, TOutput>,
|
|
27
|
-
client: NexvoraClient,
|
|
28
|
-
rateLimiter?: RateLimiterRegistry,
|
|
29
|
-
): (rawInput: unknown) => Promise<ToolCallResult> {
|
|
30
|
-
return async (rawInput: unknown): Promise<ToolCallResult> => {
|
|
31
|
-
const start = Date.now();
|
|
32
|
-
|
|
33
|
-
if (rateLimiter) {
|
|
34
|
-
const result = rateLimiter.tryConsume(definition.name);
|
|
35
|
-
if (!result.allowed) {
|
|
36
|
-
const retrySec = (result.retryAfterMs / 1000).toFixed(1);
|
|
37
|
-
await client.sendAudit({
|
|
38
|
-
toolName: definition.name,
|
|
39
|
-
outcome: "rate_limited",
|
|
40
|
-
agentId: client.agentId,
|
|
41
|
-
});
|
|
42
|
-
return {
|
|
43
|
-
content: [
|
|
44
|
-
{
|
|
45
|
-
type: "text",
|
|
46
|
-
text: `Rate limit exceeded for ${definition.name}. Retry after ${retrySec} seconds.`,
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
isError: true,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const parsed = definition.inputSchema.safeParse(rawInput);
|
|
55
|
-
if (!parsed.success) {
|
|
56
|
-
await client.sendAudit({
|
|
57
|
-
toolName: definition.name,
|
|
58
|
-
outcome: "error",
|
|
59
|
-
agentId: client.agentId,
|
|
60
|
-
errorCode: "VALIDATION_ERROR",
|
|
61
|
-
});
|
|
62
|
-
return {
|
|
63
|
-
content: [{ type: "text", text: `Invalid input: ${parsed.error.message}` }],
|
|
64
|
-
isError: true,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const result = await definition.handler(parsed.data, client);
|
|
70
|
-
const durationMs = Date.now() - start;
|
|
71
|
-
|
|
72
|
-
await client.sendAudit({
|
|
73
|
-
toolName: definition.name,
|
|
74
|
-
outcome: "success",
|
|
75
|
-
agentId: client.agentId,
|
|
76
|
-
durationMs,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
content: [
|
|
81
|
-
{
|
|
82
|
-
type: "text",
|
|
83
|
-
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
} catch (err) {
|
|
88
|
-
const durationMs = Date.now() - start;
|
|
89
|
-
|
|
90
|
-
if (err instanceof NexvoraApiError) {
|
|
91
|
-
await client.sendAudit({
|
|
92
|
-
toolName: definition.name,
|
|
93
|
-
outcome: err.toAuditOutcome(),
|
|
94
|
-
agentId: client.agentId,
|
|
95
|
-
durationMs,
|
|
96
|
-
errorCode: String(err.statusCode),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
content: [{ type: "text", text: err.message }],
|
|
101
|
-
isError: true,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
await client.sendAudit({
|
|
106
|
-
toolName: definition.name,
|
|
107
|
-
outcome: "error",
|
|
108
|
-
agentId: client.agentId,
|
|
109
|
-
durationMs,
|
|
110
|
-
errorCode: "UNEXPECTED_ERROR",
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
-
return {
|
|
115
|
-
content: [{ type: "text", text: `Unexpected error: ${message}` }],
|
|
116
|
-
isError: true,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stdio entry point — backward-compatible with all existing Claude Code /
|
|
3
|
-
* Cursor / ChatGPT Desktop configurations that launch the server as a
|
|
4
|
-
* subprocess via "command" + "args".
|
|
5
|
-
*
|
|
6
|
-
* For the SSE (remote MCP) transport or the OAuth login command, use the
|
|
7
|
-
* `nexvora` CLI (src/cli.ts → dist/cli.js).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
|
|
12
|
-
import { createServer } from "./createServer.js";
|
|
13
|
-
|
|
14
|
-
const NEXVORA_BASE_URL = process.env["NEXVORA_BASE_URL"] ?? "https://api.nxvora.online";
|
|
15
|
-
const NEXVORA_ACCESS_TOKEN = process.env["NEXVORA_ACCESS_TOKEN"] ?? "";
|
|
16
|
-
const NEXVORA_AGENT_ID = process.env["NEXVORA_AGENT_ID"];
|
|
17
|
-
|
|
18
|
-
if (!NEXVORA_ACCESS_TOKEN) {
|
|
19
|
-
console.error("Error: NEXVORA_ACCESS_TOKEN environment variable is required");
|
|
20
|
-
console.error(" Two ways to set it:");
|
|
21
|
-
console.error(" 1. Run `nexvora login` (OAuth Device Grant — auto-refreshes)");
|
|
22
|
-
console.error(" 2. Generate a Personal Access Token at");
|
|
23
|
-
console.error(" https://app.nxvora.online/app/settings/mcp-tokens and paste");
|
|
24
|
-
console.error(" the nxv_pat_... string into NEXVORA_ACCESS_TOKEN in your");
|
|
25
|
-
console.error(" mcp.json env block.");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const server = await createServer({
|
|
30
|
-
baseUrl: NEXVORA_BASE_URL,
|
|
31
|
-
accessToken: NEXVORA_ACCESS_TOKEN,
|
|
32
|
-
agentId: NEXVORA_AGENT_ID,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const transport = new StdioServerTransport();
|
|
36
|
-
await server.connect(transport);
|
package/src/server/sse.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP + SSE transport for the NexVora MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Exposes three endpoints:
|
|
5
|
-
* GET /.well-known/oauth-authorization-server — OAuth 2.0 discovery (RFC 8414)
|
|
6
|
-
* GET /health — liveness probe
|
|
7
|
-
* GET /sse — SSE stream (MCP transport)
|
|
8
|
-
* POST /messages?sessionId=<id> — MCP message delivery
|
|
9
|
-
*
|
|
10
|
-
* All /sse and /messages requests require a valid Bearer token in the
|
|
11
|
-
* Authorization header. The well-known and health endpoints are public.
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* const server = startSseServer({ port: 7700, baseUrl, accessToken });
|
|
15
|
-
* // server is a Node http.Server — call server.close() to shut down
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import * as http from "node:http";
|
|
19
|
-
|
|
20
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
21
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
|
-
|
|
23
|
-
export interface SseServerOptions {
|
|
24
|
-
port: number;
|
|
25
|
-
/** McpServer instance with all tools already registered. */
|
|
26
|
-
mcpServer: McpServer;
|
|
27
|
-
/** Base URL of the NexVora API — used to build OAuth discovery metadata. */
|
|
28
|
-
nexvoraBaseUrl: string;
|
|
29
|
-
/**
|
|
30
|
-
* Token validator. Called with the raw Bearer token string on every
|
|
31
|
-
* authenticated request. Return true to allow the request, false to 401.
|
|
32
|
-
*
|
|
33
|
-
* Default: always returns true (useful for local dev; override in production).
|
|
34
|
-
*/
|
|
35
|
-
validateToken?: (token: string) => Promise<boolean>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── OAuth 2.0 discovery metadata ──────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
function oauthMeta(nexvoraBaseUrl: string): Record<string, unknown> {
|
|
41
|
-
const base = nexvoraBaseUrl.replace(/\/$/, "");
|
|
42
|
-
return {
|
|
43
|
-
issuer: base,
|
|
44
|
-
authorization_endpoint: `${base}/oauth/authorize`,
|
|
45
|
-
token_endpoint: `${base}/oauth/token`,
|
|
46
|
-
response_types_supported: ["code"],
|
|
47
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
48
|
-
code_challenge_methods_supported: ["S256"],
|
|
49
|
-
scopes_supported: ["mcp:tools", "offline_access"],
|
|
50
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Server factory ────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
export function startSseServer(opts: SseServerOptions): http.Server {
|
|
57
|
-
const { port, mcpServer, nexvoraBaseUrl } = opts;
|
|
58
|
-
const validateToken = opts.validateToken ?? (() => Promise.resolve(true));
|
|
59
|
-
|
|
60
|
-
// Map sessionId → SSEServerTransport (one entry per connected SSE client)
|
|
61
|
-
const sessions = new Map<string, SSEServerTransport>();
|
|
62
|
-
|
|
63
|
-
const server = http.createServer(async (req, res) => {
|
|
64
|
-
const origin = req.headers["origin"] ?? "*";
|
|
65
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
66
|
-
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
67
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
68
|
-
|
|
69
|
-
// Pre-flight
|
|
70
|
-
if (req.method === "OPTIONS") {
|
|
71
|
-
res.writeHead(204);
|
|
72
|
-
res.end();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
77
|
-
|
|
78
|
-
// ── Public endpoints ───────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
if (url.pathname === "/.well-known/oauth-authorization-server" && req.method === "GET") {
|
|
81
|
-
const body = JSON.stringify(oauthMeta(nexvoraBaseUrl));
|
|
82
|
-
res.writeHead(200, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
83
|
-
res.end(body);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (url.pathname === "/health" && req.method === "GET") {
|
|
88
|
-
const body = JSON.stringify({ status: "ok", transport: "sse", sessions: sessions.size });
|
|
89
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
90
|
-
res.end(body);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ── Bearer-auth guard ──────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
const authHeader = req.headers["authorization"] ?? "";
|
|
97
|
-
if (!authHeader.startsWith("Bearer ")) {
|
|
98
|
-
jsonError(res, 401, "Bearer token required in Authorization header");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const token = authHeader.slice(7);
|
|
102
|
-
const valid = await validateToken(token);
|
|
103
|
-
if (!valid) {
|
|
104
|
-
jsonError(res, 401, "Invalid or expired token");
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ── SSE endpoint — one transport per client connection ─────────────────
|
|
109
|
-
|
|
110
|
-
if (url.pathname === "/sse" && req.method === "GET") {
|
|
111
|
-
const transport = new SSEServerTransport("/messages", res);
|
|
112
|
-
sessions.set(transport.sessionId, transport);
|
|
113
|
-
res.on("close", () => sessions.delete(transport.sessionId));
|
|
114
|
-
await mcpServer.connect(transport);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Message endpoint — client posts MCP messages here ─────────────────
|
|
119
|
-
|
|
120
|
-
if (url.pathname === "/messages" && req.method === "POST") {
|
|
121
|
-
const sessionId = url.searchParams.get("sessionId") ?? "";
|
|
122
|
-
const transport = sessions.get(sessionId);
|
|
123
|
-
if (!transport) {
|
|
124
|
-
jsonError(res, 404, `Session '${sessionId}' not found. Has the SSE connection been established?`);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
await transport.handlePostMessage(req, res);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
res.writeHead(404);
|
|
132
|
-
res.end();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
server.listen(port, () => {
|
|
136
|
-
console.error(`NexVora MCP server (SSE) listening on http://localhost:${port}/sse`);
|
|
137
|
-
console.error(`OAuth discovery at http://localhost:${port}/.well-known/oauth-authorization-server`);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
return server;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
function jsonError(res: http.ServerResponse, status: number, message: string): void {
|
|
146
|
-
const body = JSON.stringify({ error: message });
|
|
147
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
148
|
-
res.end(body);
|
|
149
|
-
}
|