noumen 0.1.0 → 0.3.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.
- package/README.md +846 -51
- package/dist/a2a/index.d.ts +148 -0
- package/dist/a2a/index.js +579 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/acp/index.d.ts +129 -0
- package/dist/acp/index.js +498 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/agent-1nFVUP9E.d.ts +1332 -0
- package/dist/cache-DsRqxx6v.d.ts +38 -0
- package/dist/chunk-3HEYCV26.js +10 -0
- package/dist/chunk-3HEYCV26.js.map +1 -0
- package/dist/chunk-3SK5GCI6.js +75 -0
- package/dist/chunk-3SK5GCI6.js.map +1 -0
- package/dist/chunk-42PHHZUA.js +132 -0
- package/dist/chunk-42PHHZUA.js.map +1 -0
- package/dist/chunk-4HW6LN6D.js +10365 -0
- package/dist/chunk-4HW6LN6D.js.map +1 -0
- package/dist/chunk-4SQA2UCV.js +26 -0
- package/dist/chunk-4SQA2UCV.js.map +1 -0
- package/dist/chunk-5GEX6ZSB.js +179 -0
- package/dist/chunk-5GEX6ZSB.js.map +1 -0
- package/dist/chunk-5JN4SPI7.js +94 -0
- package/dist/chunk-5JN4SPI7.js.map +1 -0
- package/dist/chunk-AMYIJSAZ.js +57 -0
- package/dist/chunk-AMYIJSAZ.js.map +1 -0
- package/dist/chunk-BZSFUEWM.js +43 -0
- package/dist/chunk-BZSFUEWM.js.map +1 -0
- package/dist/chunk-CS6WNDCF.js +171 -0
- package/dist/chunk-CS6WNDCF.js.map +1 -0
- package/dist/chunk-D43BWEZA.js +346 -0
- package/dist/chunk-D43BWEZA.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-EKOGVTBT.js +472 -0
- package/dist/chunk-EKOGVTBT.js.map +1 -0
- package/dist/chunk-HEQQQGK5.js +131 -0
- package/dist/chunk-HEQQQGK5.js.map +1 -0
- package/dist/chunk-HL6JCRZJ.js +3112 -0
- package/dist/chunk-HL6JCRZJ.js.map +1 -0
- package/dist/chunk-JACGEMTF.js +43 -0
- package/dist/chunk-JACGEMTF.js.map +1 -0
- package/dist/chunk-JX7CLUCV.js +21 -0
- package/dist/chunk-JX7CLUCV.js.map +1 -0
- package/dist/chunk-KXDB56YW.js +39 -0
- package/dist/chunk-KXDB56YW.js.map +1 -0
- package/dist/chunk-L3L3FG5T.js +16 -0
- package/dist/chunk-L3L3FG5T.js.map +1 -0
- package/dist/chunk-OGXNFXFA.js +196 -0
- package/dist/chunk-OGXNFXFA.js.map +1 -0
- package/dist/chunk-UVSSQBDY.js +192 -0
- package/dist/chunk-UVSSQBDY.js.map +1 -0
- package/dist/chunk-Y45R3PQL.js +684 -0
- package/dist/chunk-Y45R3PQL.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +874 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.d.ts +64 -0
- package/dist/client/index.js +409 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-CRRO2376.js +10 -0
- package/dist/client-CRRO2376.js.map +1 -0
- package/dist/headless-FFU2DESQ.js +142 -0
- package/dist/headless-FFU2DESQ.js.map +1 -0
- package/dist/history-snip-64GYP4ZL.js +12 -0
- package/dist/history-snip-64GYP4ZL.js.map +1 -0
- package/dist/index.d.ts +1459 -422
- package/dist/index.js +398 -1757
- package/dist/index.js.map +1 -1
- package/dist/jsonrpc/index.d.ts +54 -0
- package/dist/jsonrpc/index.js +34 -0
- package/dist/jsonrpc/index.js.map +1 -0
- package/dist/lsp/index.d.ts +36 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/index.js.map +1 -0
- package/dist/lsp-PS3BWIHC.js +8 -0
- package/dist/lsp-PS3BWIHC.js.map +1 -0
- package/dist/manager-DLXK63XC.js +8 -0
- package/dist/manager-DLXK63XC.js.map +1 -0
- package/dist/mcp/index.d.ts +111 -0
- package/dist/mcp/index.js +105 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-auth-AEI2R4ZC.js +9 -0
- package/dist/mcp-auth-AEI2R4ZC.js.map +1 -0
- package/dist/provider-factory-KCLIF34X.js +20 -0
- package/dist/provider-factory-KCLIF34X.js.map +1 -0
- package/dist/providers/anthropic.d.ts +19 -0
- package/dist/providers/anthropic.js +35 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/bedrock.d.ts +39 -0
- package/dist/providers/bedrock.js +56 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/gemini.d.ts +17 -0
- package/dist/providers/gemini.js +262 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.js +20 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +21 -0
- package/dist/providers/openai.js +9 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +16 -0
- package/dist/providers/openrouter.js +24 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/vertex.d.ts +42 -0
- package/dist/providers/vertex.js +67 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/render-GRN4ZSSW.js +14 -0
- package/dist/render-GRN4ZSSW.js.map +1 -0
- package/dist/resolve-4JA2BBDA.js +14 -0
- package/dist/resolve-4JA2BBDA.js.map +1 -0
- package/dist/server/index.d.ts +143 -0
- package/dist/server/index.js +695 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server-CHMxuWKq.d.ts +96 -0
- package/dist/spinner-OJNR6NFO.js +8 -0
- package/dist/spinner-OJNR6NFO.js.map +1 -0
- package/dist/types-2kTLUCnD.d.ts +107 -0
- package/dist/types-CD0rUKKT.d.ts +109 -0
- package/dist/types-LrU4LRmX.d.ts +575 -0
- package/dist/types-NIyVwQ4h.d.ts +109 -0
- package/dist/types-QwfylltH.d.ts +71 -0
- package/dist/types-RPKUTu1k.d.ts +645 -0
- package/dist/uuid-RVN2T26F.js +8 -0
- package/dist/uuid-RVN2T26F.js.map +1 -0
- package/dist/zod-7YXKWYMC.js +12 -0
- package/dist/zod-7YXKWYMC.js.map +1 -0
- package/package.json +141 -7
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import {
|
|
2
|
+
maybeResizeAndDownsampleImageBuffer
|
|
3
|
+
} from "./chunk-5GEX6ZSB.js";
|
|
4
|
+
import {
|
|
5
|
+
buildMcpToolName
|
|
6
|
+
} from "./chunk-4SQA2UCV.js";
|
|
7
|
+
|
|
8
|
+
// src/mcp/client.ts
|
|
9
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
10
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
11
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
12
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
13
|
+
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
14
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
15
|
+
|
|
16
|
+
// src/mcp/auth/provider.ts
|
|
17
|
+
import { randomBytes } from "crypto";
|
|
18
|
+
import { exec } from "child_process";
|
|
19
|
+
var NoumenOAuthProvider = class {
|
|
20
|
+
serverKey;
|
|
21
|
+
storage;
|
|
22
|
+
_clientMetadata;
|
|
23
|
+
_callbackPort;
|
|
24
|
+
_onAuthorizationUrl;
|
|
25
|
+
_signal;
|
|
26
|
+
_preRegisteredClientId;
|
|
27
|
+
_preRegisteredClientSecret;
|
|
28
|
+
_state = null;
|
|
29
|
+
constructor(serverKey, options) {
|
|
30
|
+
this.serverKey = serverKey;
|
|
31
|
+
this.storage = options.storage;
|
|
32
|
+
this._clientMetadata = options.clientMetadata;
|
|
33
|
+
this._callbackPort = options.callbackPort ?? 3118;
|
|
34
|
+
this._onAuthorizationUrl = options.onAuthorizationUrl;
|
|
35
|
+
this._signal = options.signal;
|
|
36
|
+
this._preRegisteredClientId = options.clientId;
|
|
37
|
+
this._preRegisteredClientSecret = options.clientSecret;
|
|
38
|
+
}
|
|
39
|
+
get redirectUrl() {
|
|
40
|
+
return `http://localhost:${this._callbackPort}/callback`;
|
|
41
|
+
}
|
|
42
|
+
get clientMetadata() {
|
|
43
|
+
return this._clientMetadata;
|
|
44
|
+
}
|
|
45
|
+
async state() {
|
|
46
|
+
if (!this._state) {
|
|
47
|
+
this._state = randomBytes(32).toString("base64url");
|
|
48
|
+
}
|
|
49
|
+
return this._state;
|
|
50
|
+
}
|
|
51
|
+
async clientInformation() {
|
|
52
|
+
if (this._preRegisteredClientId) {
|
|
53
|
+
const info = {
|
|
54
|
+
client_id: this._preRegisteredClientId
|
|
55
|
+
};
|
|
56
|
+
if (this._preRegisteredClientSecret) {
|
|
57
|
+
info.client_secret = this._preRegisteredClientSecret;
|
|
58
|
+
}
|
|
59
|
+
return info;
|
|
60
|
+
}
|
|
61
|
+
const data = await this.storage.load(this.serverKey);
|
|
62
|
+
return data?.clientInformation;
|
|
63
|
+
}
|
|
64
|
+
async saveClientInformation(clientInformation) {
|
|
65
|
+
const data = await this.storage.load(this.serverKey) ?? {};
|
|
66
|
+
data.clientInformation = clientInformation;
|
|
67
|
+
await this.storage.save(this.serverKey, data);
|
|
68
|
+
}
|
|
69
|
+
async tokens() {
|
|
70
|
+
const data = await this.storage.load(this.serverKey);
|
|
71
|
+
if (!data?.tokens) return void 0;
|
|
72
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
73
|
+
if (data.expiresAt && data.tokens.refresh_token && Date.now() >= data.expiresAt - REFRESH_BUFFER_MS) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
return data.tokens;
|
|
77
|
+
}
|
|
78
|
+
async saveTokens(tokens) {
|
|
79
|
+
const data = await this.storage.load(this.serverKey) ?? {};
|
|
80
|
+
data.tokens = tokens;
|
|
81
|
+
if (tokens.expires_in != null) {
|
|
82
|
+
data.expiresAt = Date.now() + tokens.expires_in * 1e3;
|
|
83
|
+
}
|
|
84
|
+
await this.storage.save(this.serverKey, data);
|
|
85
|
+
}
|
|
86
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
87
|
+
const urlStr = authorizationUrl.toString();
|
|
88
|
+
if (this._onAuthorizationUrl) {
|
|
89
|
+
await this._onAuthorizationUrl(urlStr);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
openBrowser(urlStr);
|
|
93
|
+
}
|
|
94
|
+
async saveCodeVerifier(codeVerifier) {
|
|
95
|
+
const data = await this.storage.load(this.serverKey) ?? {};
|
|
96
|
+
data.codeVerifier = codeVerifier;
|
|
97
|
+
await this.storage.save(this.serverKey, data);
|
|
98
|
+
}
|
|
99
|
+
async codeVerifier() {
|
|
100
|
+
const data = await this.storage.load(this.serverKey);
|
|
101
|
+
return data?.codeVerifier ?? "";
|
|
102
|
+
}
|
|
103
|
+
async invalidateCredentials(scope) {
|
|
104
|
+
if (scope === "all") {
|
|
105
|
+
this._state = null;
|
|
106
|
+
await this.storage.delete(this.serverKey);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const data = await this.storage.load(this.serverKey);
|
|
110
|
+
if (!data) return;
|
|
111
|
+
switch (scope) {
|
|
112
|
+
case "client":
|
|
113
|
+
delete data.clientInformation;
|
|
114
|
+
break;
|
|
115
|
+
case "tokens":
|
|
116
|
+
delete data.tokens;
|
|
117
|
+
delete data.expiresAt;
|
|
118
|
+
break;
|
|
119
|
+
case "verifier":
|
|
120
|
+
delete data.codeVerifier;
|
|
121
|
+
break;
|
|
122
|
+
case "discovery":
|
|
123
|
+
delete data.discoveryState;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
await this.storage.save(this.serverKey, data);
|
|
127
|
+
}
|
|
128
|
+
async saveDiscoveryState(state) {
|
|
129
|
+
const data = await this.storage.load(this.serverKey) ?? {};
|
|
130
|
+
data.discoveryState = state;
|
|
131
|
+
await this.storage.save(this.serverKey, data);
|
|
132
|
+
}
|
|
133
|
+
async discoveryState() {
|
|
134
|
+
const data = await this.storage.load(this.serverKey);
|
|
135
|
+
return data?.discoveryState;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
function openBrowser(url) {
|
|
139
|
+
const platform = process.platform;
|
|
140
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
141
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/mcp/auth/callback-server.ts
|
|
145
|
+
import * as http from "http";
|
|
146
|
+
var DEFAULT_FALLBACK_PORT = 3118;
|
|
147
|
+
var EPHEMERAL_PORT_MIN = 49152;
|
|
148
|
+
var EPHEMERAL_PORT_MAX = 65535;
|
|
149
|
+
var MAX_PORT_ATTEMPTS = 50;
|
|
150
|
+
var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
151
|
+
async function findAvailablePort(preferred) {
|
|
152
|
+
if (preferred != null) {
|
|
153
|
+
const ok = await isPortAvailable(preferred);
|
|
154
|
+
if (ok) return preferred;
|
|
155
|
+
}
|
|
156
|
+
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
157
|
+
const port = EPHEMERAL_PORT_MIN + Math.floor(Math.random() * (EPHEMERAL_PORT_MAX - EPHEMERAL_PORT_MIN + 1));
|
|
158
|
+
const ok = await isPortAvailable(port);
|
|
159
|
+
if (ok) return port;
|
|
160
|
+
}
|
|
161
|
+
return DEFAULT_FALLBACK_PORT;
|
|
162
|
+
}
|
|
163
|
+
function isPortAvailable(port) {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
const server = http.createServer();
|
|
166
|
+
server.once("error", () => resolve(false));
|
|
167
|
+
server.listen(port, "127.0.0.1", () => {
|
|
168
|
+
server.close(() => resolve(true));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
var OAuthCallbackServer = class {
|
|
173
|
+
server = null;
|
|
174
|
+
port = 0;
|
|
175
|
+
/**
|
|
176
|
+
* Start listening on a local port and return a promise that resolves
|
|
177
|
+
* when the authorization callback is received.
|
|
178
|
+
*/
|
|
179
|
+
async start(options) {
|
|
180
|
+
const port = await findAvailablePort(options?.callbackPort);
|
|
181
|
+
this.port = port;
|
|
182
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
183
|
+
const callbackPromise = new Promise(
|
|
184
|
+
(resolve, reject) => {
|
|
185
|
+
const server = http.createServer((req, res) => {
|
|
186
|
+
if (!req.url?.startsWith("/callback")) {
|
|
187
|
+
res.writeHead(404);
|
|
188
|
+
res.end("Not found");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const parsed = new URL(req.url, `http://localhost:${port}`);
|
|
192
|
+
const code = parsed.searchParams.get("code") ?? void 0;
|
|
193
|
+
const state = parsed.searchParams.get("state") ?? void 0;
|
|
194
|
+
const error = parsed.searchParams.get("error") ?? void 0;
|
|
195
|
+
const errorDescription = parsed.searchParams.get("error_description") ?? void 0;
|
|
196
|
+
if (error) {
|
|
197
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
198
|
+
res.end(
|
|
199
|
+
`<html><body><h1>Authorization Error</h1><p>${escapeHtml(error)}: ${escapeHtml(errorDescription ?? "")}</p></body></html>`
|
|
200
|
+
);
|
|
201
|
+
cleanup();
|
|
202
|
+
reject(new Error(`OAuth error: ${error} - ${errorDescription ?? ""}`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!code) {
|
|
206
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
207
|
+
res.end(
|
|
208
|
+
"<html><body><h1>Error</h1><p>Missing authorization code.</p></body></html>"
|
|
209
|
+
);
|
|
210
|
+
cleanup();
|
|
211
|
+
reject(new Error("Missing authorization code in callback"));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (options?.expectedState && state !== options.expectedState) {
|
|
215
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
216
|
+
res.end(
|
|
217
|
+
"<html><body><h1>Error</h1><p>State parameter mismatch.</p></body></html>"
|
|
218
|
+
);
|
|
219
|
+
cleanup();
|
|
220
|
+
reject(new Error("OAuth state mismatch"));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
224
|
+
res.end(
|
|
225
|
+
"<html><body><h1>Authorization Successful</h1><p>You can close this window.</p></body></html>"
|
|
226
|
+
);
|
|
227
|
+
cleanup();
|
|
228
|
+
resolve({ code, state });
|
|
229
|
+
});
|
|
230
|
+
this.server = server;
|
|
231
|
+
const timeout = setTimeout(() => {
|
|
232
|
+
cleanup();
|
|
233
|
+
reject(new Error("OAuth callback timeout"));
|
|
234
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
235
|
+
const cleanup = () => {
|
|
236
|
+
clearTimeout(timeout);
|
|
237
|
+
this.close();
|
|
238
|
+
};
|
|
239
|
+
if (options?.signal) {
|
|
240
|
+
if (options.signal.aborted) {
|
|
241
|
+
cleanup();
|
|
242
|
+
reject(new Error("OAuth callback aborted"));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
options.signal.addEventListener(
|
|
246
|
+
"abort",
|
|
247
|
+
() => {
|
|
248
|
+
cleanup();
|
|
249
|
+
reject(new Error("OAuth callback aborted"));
|
|
250
|
+
},
|
|
251
|
+
{ once: true }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
server.listen(port, "127.0.0.1");
|
|
255
|
+
server.unref();
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
return { port, redirectUri, waitForCallback: () => callbackPromise };
|
|
259
|
+
}
|
|
260
|
+
close() {
|
|
261
|
+
if (this.server) {
|
|
262
|
+
try {
|
|
263
|
+
this.server.close();
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
this.server = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function escapeHtml(s) {
|
|
271
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/mcp/auth/storage.ts
|
|
275
|
+
import * as fs from "fs/promises";
|
|
276
|
+
import * as path from "path";
|
|
277
|
+
var InMemoryTokenStorage = class {
|
|
278
|
+
store = /* @__PURE__ */ new Map();
|
|
279
|
+
async load(serverKey) {
|
|
280
|
+
return this.store.get(serverKey);
|
|
281
|
+
}
|
|
282
|
+
async save(serverKey, data) {
|
|
283
|
+
this.store.set(serverKey, data);
|
|
284
|
+
}
|
|
285
|
+
async delete(serverKey) {
|
|
286
|
+
this.store.delete(serverKey);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var FileTokenStorage = class {
|
|
290
|
+
filePath;
|
|
291
|
+
constructor(filePath) {
|
|
292
|
+
this.filePath = filePath ?? path.join(
|
|
293
|
+
process.env.HOME ?? process.env.USERPROFILE ?? ".",
|
|
294
|
+
".noumen",
|
|
295
|
+
"mcp-oauth-tokens.json"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
async load(serverKey) {
|
|
299
|
+
const all = await this.readAll();
|
|
300
|
+
return all[serverKey];
|
|
301
|
+
}
|
|
302
|
+
async save(serverKey, data) {
|
|
303
|
+
const all = await this.readAll();
|
|
304
|
+
all[serverKey] = data;
|
|
305
|
+
await this.writeAll(all);
|
|
306
|
+
}
|
|
307
|
+
async delete(serverKey) {
|
|
308
|
+
const all = await this.readAll();
|
|
309
|
+
delete all[serverKey];
|
|
310
|
+
await this.writeAll(all);
|
|
311
|
+
}
|
|
312
|
+
async readAll() {
|
|
313
|
+
try {
|
|
314
|
+
const raw = await fs.readFile(this.filePath, "utf-8");
|
|
315
|
+
return JSON.parse(raw);
|
|
316
|
+
} catch {
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async writeAll(data) {
|
|
321
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
322
|
+
await fs.writeFile(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/mcp/client.ts
|
|
327
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 6e4;
|
|
328
|
+
var MAX_TEXT_RESULT_BYTES = 1024 * 1024;
|
|
329
|
+
var McpClientManager = class {
|
|
330
|
+
connections = /* @__PURE__ */ new Map();
|
|
331
|
+
serverConfigs;
|
|
332
|
+
tokenStorage;
|
|
333
|
+
onAuthorizationUrl;
|
|
334
|
+
constructor(mcpServers, options) {
|
|
335
|
+
this.serverConfigs = mcpServers;
|
|
336
|
+
this.tokenStorage = options?.tokenStorage ?? new InMemoryTokenStorage();
|
|
337
|
+
this.onAuthorizationUrl = options?.onAuthorizationUrl;
|
|
338
|
+
}
|
|
339
|
+
async connect() {
|
|
340
|
+
const entries = Object.entries(this.serverConfigs);
|
|
341
|
+
const results = await Promise.allSettled(
|
|
342
|
+
entries.map(([name, config]) => this.connectToServer(name, config))
|
|
343
|
+
);
|
|
344
|
+
for (let i = 0; i < results.length; i++) {
|
|
345
|
+
const result = results[i];
|
|
346
|
+
const name = entries[i][0];
|
|
347
|
+
if (result.status === "rejected") {
|
|
348
|
+
this.connections.set(name, {
|
|
349
|
+
name,
|
|
350
|
+
client: null,
|
|
351
|
+
status: "failed",
|
|
352
|
+
config: entries[i][1],
|
|
353
|
+
cleanup: async () => {
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async connectToServer(name, config) {
|
|
360
|
+
const client = new Client({ name: `noumen-${name}`, version: "0.1.0" });
|
|
361
|
+
let transport;
|
|
362
|
+
let cleanup;
|
|
363
|
+
const configType = "type" in config ? config.type : "stdio";
|
|
364
|
+
switch (configType) {
|
|
365
|
+
case "http": {
|
|
366
|
+
const httpConfig = config;
|
|
367
|
+
const url = new URL(httpConfig.url);
|
|
368
|
+
const authProvider = await this.resolveAuthProvider(name, httpConfig);
|
|
369
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
370
|
+
authProvider: authProvider ?? void 0,
|
|
371
|
+
requestInit: httpConfig.headers ? { headers: httpConfig.headers } : void 0
|
|
372
|
+
});
|
|
373
|
+
cleanup = async () => {
|
|
374
|
+
await transport.close();
|
|
375
|
+
};
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "sse": {
|
|
379
|
+
const sseConfig = config;
|
|
380
|
+
const url = new URL(sseConfig.url);
|
|
381
|
+
const authProvider = await this.resolveAuthProvider(name, sseConfig);
|
|
382
|
+
transport = new SSEClientTransport(url, {
|
|
383
|
+
authProvider: authProvider ?? void 0,
|
|
384
|
+
requestInit: sseConfig.headers ? { headers: sseConfig.headers } : void 0
|
|
385
|
+
});
|
|
386
|
+
cleanup = async () => {
|
|
387
|
+
await transport.close();
|
|
388
|
+
};
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "websocket": {
|
|
392
|
+
const wsConfig = config;
|
|
393
|
+
const url = new URL(wsConfig.url);
|
|
394
|
+
transport = new WebSocketClientTransport(url);
|
|
395
|
+
cleanup = async () => {
|
|
396
|
+
await transport.close();
|
|
397
|
+
};
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
default: {
|
|
401
|
+
const stdioConfig = config;
|
|
402
|
+
transport = new StdioClientTransport({
|
|
403
|
+
command: stdioConfig.command,
|
|
404
|
+
args: stdioConfig.args,
|
|
405
|
+
env: stdioConfig.env ? { ...process.env, ...stdioConfig.env } : void 0
|
|
406
|
+
});
|
|
407
|
+
cleanup = async () => {
|
|
408
|
+
await transport.close();
|
|
409
|
+
};
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
await client.connect(transport);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (err instanceof UnauthorizedError) {
|
|
417
|
+
this.connections.set(name, {
|
|
418
|
+
name,
|
|
419
|
+
client,
|
|
420
|
+
status: "needs-auth",
|
|
421
|
+
config,
|
|
422
|
+
cleanup
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
this.connections.set(name, {
|
|
429
|
+
name,
|
|
430
|
+
client,
|
|
431
|
+
status: "connected",
|
|
432
|
+
config,
|
|
433
|
+
cleanup
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Resolve an OAuthClientProvider for an HTTP or SSE server config.
|
|
438
|
+
* Returns null if the server doesn't require authentication.
|
|
439
|
+
*/
|
|
440
|
+
async resolveAuthProvider(serverName, config) {
|
|
441
|
+
if (config.authProvider) return config.authProvider;
|
|
442
|
+
if (!config.oauth) return null;
|
|
443
|
+
const oauth = config.oauth;
|
|
444
|
+
const serverKey = `${serverName}|${config.url}`;
|
|
445
|
+
const port = await findAvailablePort(oauth.callbackPort);
|
|
446
|
+
return new NoumenOAuthProvider(serverKey, {
|
|
447
|
+
storage: this.tokenStorage,
|
|
448
|
+
clientId: oauth.clientId,
|
|
449
|
+
clientSecret: oauth.clientSecret,
|
|
450
|
+
callbackPort: port,
|
|
451
|
+
onAuthorizationUrl: this.onAuthorizationUrl,
|
|
452
|
+
clientMetadata: {
|
|
453
|
+
redirect_uris: [`http://localhost:${port}/callback`],
|
|
454
|
+
client_name: `noumen-${serverName}`,
|
|
455
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
456
|
+
response_types: ["code"],
|
|
457
|
+
scope: oauth.scopes
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async getTools() {
|
|
462
|
+
const tools = [];
|
|
463
|
+
for (const [serverName, conn] of this.connections) {
|
|
464
|
+
if (conn.status !== "connected" || !conn.client) continue;
|
|
465
|
+
try {
|
|
466
|
+
const result = await conn.client.listTools();
|
|
467
|
+
for (const mcpTool of result.tools) {
|
|
468
|
+
tools.push(this.mapMcpTool(serverName, mcpTool));
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return tools;
|
|
474
|
+
}
|
|
475
|
+
mapMcpTool(serverName, mcpTool) {
|
|
476
|
+
const qualifiedName = buildMcpToolName(serverName, mcpTool.name);
|
|
477
|
+
const parameters = mcpTool.inputSchema ?? {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {}
|
|
480
|
+
};
|
|
481
|
+
const mcpInfo = {
|
|
482
|
+
serverName,
|
|
483
|
+
toolName: mcpTool.name
|
|
484
|
+
};
|
|
485
|
+
return {
|
|
486
|
+
name: qualifiedName,
|
|
487
|
+
description: mcpTool.description ?? "",
|
|
488
|
+
parameters,
|
|
489
|
+
mcpInfo,
|
|
490
|
+
call: async (args) => {
|
|
491
|
+
return this.callTool(serverName, mcpTool.name, args);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async callTool(serverName, toolName, args, options) {
|
|
496
|
+
const conn = this.connections.get(serverName);
|
|
497
|
+
if (!conn || conn.status !== "connected" || !conn.client) {
|
|
498
|
+
return {
|
|
499
|
+
content: `MCP server "${serverName}" is not connected`,
|
|
500
|
+
isError: true
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
505
|
+
const abortController = new AbortController();
|
|
506
|
+
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
507
|
+
let result;
|
|
508
|
+
try {
|
|
509
|
+
result = await conn.client.callTool(
|
|
510
|
+
{ name: toolName, arguments: args },
|
|
511
|
+
void 0,
|
|
512
|
+
{ signal: abortController.signal }
|
|
513
|
+
);
|
|
514
|
+
} finally {
|
|
515
|
+
clearTimeout(timer);
|
|
516
|
+
}
|
|
517
|
+
const contentBlocks = result.content;
|
|
518
|
+
if (!contentBlocks) {
|
|
519
|
+
return { content: JSON.stringify(result), isError: result.isError === true };
|
|
520
|
+
}
|
|
521
|
+
const parts = [];
|
|
522
|
+
for (const block of contentBlocks) {
|
|
523
|
+
if (block.type === "text") {
|
|
524
|
+
parts.push({ type: "text", text: block.text ?? "" });
|
|
525
|
+
} else if (block.type === "image" && block.data) {
|
|
526
|
+
const imageBuffer = Buffer.from(block.data, "base64");
|
|
527
|
+
const ext = block.mimeType?.split("/")[1] || "png";
|
|
528
|
+
try {
|
|
529
|
+
const resized = await maybeResizeAndDownsampleImageBuffer(
|
|
530
|
+
imageBuffer,
|
|
531
|
+
imageBuffer.length,
|
|
532
|
+
ext
|
|
533
|
+
);
|
|
534
|
+
parts.push({
|
|
535
|
+
type: "image",
|
|
536
|
+
data: resized.buffer.toString("base64"),
|
|
537
|
+
media_type: `image/${resized.mediaType}`
|
|
538
|
+
});
|
|
539
|
+
} catch {
|
|
540
|
+
parts.push({
|
|
541
|
+
type: "image",
|
|
542
|
+
data: block.data,
|
|
543
|
+
media_type: block.mimeType ?? "image/png"
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} else if (block.type === "resource" && block.blob) {
|
|
547
|
+
const isImage = block.mimeType?.startsWith("image/") ?? false;
|
|
548
|
+
if (isImage) {
|
|
549
|
+
const imageBuffer = Buffer.from(block.blob, "base64");
|
|
550
|
+
const ext = block.mimeType?.split("/")[1] || "png";
|
|
551
|
+
try {
|
|
552
|
+
const resized = await maybeResizeAndDownsampleImageBuffer(
|
|
553
|
+
imageBuffer,
|
|
554
|
+
imageBuffer.length,
|
|
555
|
+
ext
|
|
556
|
+
);
|
|
557
|
+
parts.push({
|
|
558
|
+
type: "image",
|
|
559
|
+
data: resized.buffer.toString("base64"),
|
|
560
|
+
media_type: `image/${resized.mediaType}`
|
|
561
|
+
});
|
|
562
|
+
} catch {
|
|
563
|
+
parts.push({
|
|
564
|
+
type: "image",
|
|
565
|
+
data: block.blob,
|
|
566
|
+
media_type: block.mimeType ?? "image/png"
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
parts.push({ type: "text", text: JSON.stringify(block) });
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
parts.push({ type: "text", text: JSON.stringify(block) });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (parts.every((p) => p.type === "text")) {
|
|
577
|
+
let text = parts.map((p) => p.text).join("\n");
|
|
578
|
+
if (Buffer.byteLength(text, "utf-8") > MAX_TEXT_RESULT_BYTES) {
|
|
579
|
+
text = text.slice(0, MAX_TEXT_RESULT_BYTES) + "\n...[output truncated]";
|
|
580
|
+
}
|
|
581
|
+
return { content: text, isError: result.isError === true };
|
|
582
|
+
}
|
|
583
|
+
return { content: parts, isError: result.isError === true };
|
|
584
|
+
} catch (err) {
|
|
585
|
+
const isAbort = err instanceof DOMException && err.name === "AbortError";
|
|
586
|
+
const message = isAbort ? `MCP tool "${toolName}" on server "${serverName}" timed out` : err instanceof Error ? err.message : String(err);
|
|
587
|
+
return { content: message, isError: true };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
getConnectionStatus() {
|
|
591
|
+
return Array.from(this.connections.values()).map((c) => ({
|
|
592
|
+
name: c.name,
|
|
593
|
+
status: c.status
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Returns server names that are in `needs-auth` status and require
|
|
598
|
+
* interactive OAuth before they can be used.
|
|
599
|
+
*/
|
|
600
|
+
getServersNeedingAuth() {
|
|
601
|
+
return Array.from(this.connections.entries()).filter(([, conn]) => conn.status === "needs-auth").map(([name]) => name);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Reconnect a server by closing its existing connection and
|
|
605
|
+
* establishing a new one. Useful after completing OAuth.
|
|
606
|
+
*/
|
|
607
|
+
async reconnect(serverName) {
|
|
608
|
+
const conn = this.connections.get(serverName);
|
|
609
|
+
if (conn) {
|
|
610
|
+
await conn.cleanup().catch(() => {
|
|
611
|
+
});
|
|
612
|
+
this.connections.delete(serverName);
|
|
613
|
+
}
|
|
614
|
+
const config = this.serverConfigs[serverName];
|
|
615
|
+
if (!config) return;
|
|
616
|
+
try {
|
|
617
|
+
await this.connectToServer(serverName, config);
|
|
618
|
+
} catch {
|
|
619
|
+
this.connections.set(serverName, {
|
|
620
|
+
name: serverName,
|
|
621
|
+
client: null,
|
|
622
|
+
status: "failed",
|
|
623
|
+
config,
|
|
624
|
+
cleanup: async () => {
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Trigger interactive OAuth for a `needs-auth` server, then reconnect.
|
|
631
|
+
* Runs the full MCP SDK auth orchestrator with a local callback server.
|
|
632
|
+
*
|
|
633
|
+
* Returns the authorization URL if the flow requires user interaction,
|
|
634
|
+
* or null if the server connected without browser auth (e.g. cached tokens).
|
|
635
|
+
*/
|
|
636
|
+
async performAuth(serverName, options) {
|
|
637
|
+
const config = this.serverConfigs[serverName];
|
|
638
|
+
if (!config) {
|
|
639
|
+
throw new Error(`Unknown MCP server: ${serverName}`);
|
|
640
|
+
}
|
|
641
|
+
const configType = "type" in config ? config.type : "stdio";
|
|
642
|
+
if (configType !== "http" && configType !== "sse") {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`OAuth is only supported for HTTP and SSE transports, got: ${configType}`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const httpConfig = config;
|
|
648
|
+
if (!httpConfig.oauth && !httpConfig.authProvider) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`Server "${serverName}" has no OAuth configuration`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
let capturedAuthUrl;
|
|
654
|
+
const originalCallback = this.onAuthorizationUrl;
|
|
655
|
+
this.onAuthorizationUrl = async (url) => {
|
|
656
|
+
capturedAuthUrl = url;
|
|
657
|
+
if (originalCallback) await originalCallback(url);
|
|
658
|
+
};
|
|
659
|
+
try {
|
|
660
|
+
await this.reconnect(serverName);
|
|
661
|
+
} finally {
|
|
662
|
+
this.onAuthorizationUrl = originalCallback;
|
|
663
|
+
}
|
|
664
|
+
return { authUrl: capturedAuthUrl };
|
|
665
|
+
}
|
|
666
|
+
async close() {
|
|
667
|
+
const cleanups = Array.from(this.connections.values()).map(
|
|
668
|
+
(c) => c.cleanup().catch(() => {
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
await Promise.all(cleanups);
|
|
672
|
+
this.connections.clear();
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
export {
|
|
677
|
+
NoumenOAuthProvider,
|
|
678
|
+
findAvailablePort,
|
|
679
|
+
OAuthCallbackServer,
|
|
680
|
+
InMemoryTokenStorage,
|
|
681
|
+
FileTokenStorage,
|
|
682
|
+
McpClientManager
|
|
683
|
+
};
|
|
684
|
+
//# sourceMappingURL=chunk-Y45R3PQL.js.map
|