@mcp-use/client 2.2.4 → 2.2.5
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/dist/.tsbuildinfo +1 -0
- package/dist/auth/browser.d.ts +218 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/callback.d.ts +9 -0
- package/dist/auth/callback.d.ts.map +1 -0
- package/dist/auth/flow.d.ts +32 -0
- package/dist/auth/flow.d.ts.map +1 -0
- package/dist/auth/node.d.ts +212 -0
- package/dist/auth/node.d.ts.map +1 -0
- package/dist/auth/popup.d.ts +67 -0
- package/dist/auth/popup.d.ts.map +1 -0
- package/dist/auth/session-store.d.ts +125 -0
- package/dist/auth/session-store.d.ts.map +1 -0
- package/dist/auth/storage-file.d.ts +18 -0
- package/dist/auth/storage-file.d.ts.map +1 -0
- package/dist/auth/storage.d.ts +34 -0
- package/dist/auth/storage.d.ts.map +1 -0
- package/dist/auth/url.d.ts +17 -0
- package/dist/auth/url.d.ts.map +1 -0
- package/dist/code-mode/connector.d.ts +27 -0
- package/dist/code-mode/connector.d.ts.map +1 -0
- package/dist/code-mode/executor-e2b.d.ts +46 -0
- package/dist/code-mode/executor-e2b.d.ts.map +1 -0
- package/dist/code-mode/executor-vm.d.ts +39 -0
- package/dist/code-mode/executor-vm.d.ts.map +1 -0
- package/dist/code-mode/executor.d.ts +81 -0
- package/dist/code-mode/executor.d.ts.map +1 -0
- package/dist/core/base.d.ts +423 -0
- package/dist/core/base.d.ts.map +1 -0
- package/dist/core/browser.d.ts +32 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/config.d.ts +225 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/node.d.ts +437 -0
- package/dist/core/node.d.ts.map +1 -0
- package/dist/core/session.d.ts +792 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/skills.d.ts +47 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/index-browser.d.ts +26 -0
- package/dist/index-browser.d.ts.map +1 -0
- package/dist/index-browser.js +4767 -0
- package/dist/index-browser.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6113 -0
- package/dist/index.js.map +1 -0
- package/dist/react/McpClientProvider.d.ts +254 -0
- package/dist/react/McpClientProvider.d.ts.map +1 -0
- package/dist/react/index.d.ts +39 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +9444 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/rpc-logger.d.ts +47 -0
- package/dist/react/rpc-logger.d.ts.map +1 -0
- package/dist/react/storage.d.ts +98 -0
- package/dist/react/storage.d.ts.map +1 -0
- package/dist/react/token-expiry.d.ts +9 -0
- package/dist/react/token-expiry.d.ts.map +1 -0
- package/dist/react/types.d.ts +727 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/react/useMcp-helpers.d.ts +99 -0
- package/dist/react/useMcp-helpers.d.ts.map +1 -0
- package/dist/react/useMcp-operations.d.ts +180 -0
- package/dist/react/useMcp-operations.d.ts.map +1 -0
- package/dist/react/useMcp.d.ts +48 -0
- package/dist/react/useMcp.d.ts.map +1 -0
- package/dist/react/useMcpServerQueues.d.ts +403 -0
- package/dist/react/useMcpServerQueues.d.ts.map +1 -0
- package/dist/react/view/ViewRenderer.d.ts +20 -0
- package/dist/react/view/ViewRenderer.d.ts.map +1 -0
- package/dist/react/view/ext-apps-bridge.d.ts +3 -0
- package/dist/react/view/ext-apps-bridge.d.ts.map +1 -0
- package/dist/react/view/initialized-sync.d.ts +13 -0
- package/dist/react/view/initialized-sync.d.ts.map +1 -0
- package/dist/react/view/inject-openai-file-apis.d.ts +9 -0
- package/dist/react/view/inject-openai-file-apis.d.ts.map +1 -0
- package/dist/react/view/parse-custom-props.d.ts +10 -0
- package/dist/react/view/parse-custom-props.d.ts.map +1 -0
- package/dist/react/view/resolve-view-resource.d.ts +22 -0
- package/dist/react/view/resolve-view-resource.d.ts.map +1 -0
- package/dist/react/view/sandbox-blob-url.d.ts +27 -0
- package/dist/react/view/sandbox-blob-url.d.ts.map +1 -0
- package/dist/react/view/types.d.ts +220 -0
- package/dist/react/view/types.d.ts.map +1 -0
- package/dist/react/view/use-display-mode.d.ts +19 -0
- package/dist/react/view/use-display-mode.d.ts.map +1 -0
- package/dist/react/view/view-detection.d.ts +22 -0
- package/dist/react/view/view-detection.d.ts.map +1 -0
- package/dist/react/view/view-host-policy.d.ts +61 -0
- package/dist/react/view/view-host-policy.d.ts.map +1 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +288 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/telemetry/client-telemetry.d.ts +13 -0
- package/dist/telemetry/client-telemetry.d.ts.map +1 -0
- package/dist/telemetry/configure-browser.d.ts +2 -0
- package/dist/telemetry/configure-browser.d.ts.map +1 -0
- package/dist/telemetry/configure-node.d.ts +2 -0
- package/dist/telemetry/configure-node.d.ts.map +1 -0
- package/dist/telemetry/connector-telemetry.d.ts +8 -0
- package/dist/telemetry/connector-telemetry.d.ts.map +1 -0
- package/dist/telemetry/events.d.ts +91 -0
- package/dist/telemetry/events.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/tel-fetch.d.ts +19 -0
- package/dist/telemetry/tel-fetch.d.ts.map +1 -0
- package/dist/telemetry/telemetry-browser.d.ts +6 -0
- package/dist/telemetry/telemetry-browser.d.ts.map +1 -0
- package/dist/telemetry/telemetry-node.d.ts +8 -0
- package/dist/telemetry/telemetry-node.d.ts.map +1 -0
- package/dist/telemetry/telemetry.d.ts +75 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -0
- package/dist/transport/base.d.ts +656 -0
- package/dist/transport/base.d.ts.map +1 -0
- package/dist/transport/connection-manager.d.ts +59 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/http.d.ts +137 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/stdio.d.ts +97 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/utils/elicitation.d.ts +54 -0
- package/dist/utils/elicitation.d.ts.map +1 -0
- package/dist/utils/favicon.d.ts +6 -0
- package/dist/utils/favicon.d.ts.map +1 -0
- package/dist/utils/json-schema-validator.d.ts +13 -0
- package/dist/utils/json-schema-validator.d.ts.map +1 -0
- package/dist/utils/logging.d.ts +33 -0
- package/dist/utils/logging.d.ts.map +1 -0
- package/dist/utils/version.d.ts +9 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,4767 @@
|
|
|
1
|
+
// src/telemetry/client-telemetry.ts
|
|
2
|
+
var tracker;
|
|
3
|
+
function setClientTelemetryTracker(nextTracker) {
|
|
4
|
+
tracker = nextTracker;
|
|
5
|
+
}
|
|
6
|
+
function trackClientAddServer(name, config) {
|
|
7
|
+
void tracker?.addServer(name, config);
|
|
8
|
+
}
|
|
9
|
+
function trackClientRemoveServer(name) {
|
|
10
|
+
void tracker?.removeServer(name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/telemetry/connector-telemetry.ts
|
|
14
|
+
var tracker2;
|
|
15
|
+
function setConnectorTelemetryTracker(nextTracker) {
|
|
16
|
+
tracker2 = nextTracker;
|
|
17
|
+
}
|
|
18
|
+
function trackConnectorTelemetry(data) {
|
|
19
|
+
void tracker2?.(data);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/utils/logging.ts
|
|
23
|
+
var LEVELS = [
|
|
24
|
+
"silent",
|
|
25
|
+
"error",
|
|
26
|
+
"warn",
|
|
27
|
+
"info",
|
|
28
|
+
"http",
|
|
29
|
+
"verbose",
|
|
30
|
+
"debug",
|
|
31
|
+
"silly"
|
|
32
|
+
];
|
|
33
|
+
var EMOJI = {
|
|
34
|
+
silent: "",
|
|
35
|
+
error: "\u274C",
|
|
36
|
+
warn: "\u26A0\uFE0F",
|
|
37
|
+
info: "\u2139\uFE0F",
|
|
38
|
+
http: "\u{1F310}",
|
|
39
|
+
verbose: "\u{1F4DD}",
|
|
40
|
+
debug: "\u{1F50D}",
|
|
41
|
+
silly: "\u{1F92A}"
|
|
42
|
+
};
|
|
43
|
+
function envLevel() {
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = typeof process !== "undefined" ? process.env?.MCP_USE_LOG_LEVEL ?? process.env?.DEBUG : void 0;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
const v = raw?.trim().toLowerCase();
|
|
50
|
+
if (v === "2") return "debug";
|
|
51
|
+
if (v && LEVELS.includes(v)) return v;
|
|
52
|
+
return "info";
|
|
53
|
+
}
|
|
54
|
+
var SimpleConsoleLogger = class {
|
|
55
|
+
constructor(name = "mcp-use", level = "info", format = "minimal") {
|
|
56
|
+
this.name = name;
|
|
57
|
+
this.level = level;
|
|
58
|
+
this.format = format;
|
|
59
|
+
}
|
|
60
|
+
write(level, message, args) {
|
|
61
|
+
if (this.level === "silent" || LEVELS.indexOf(level) > LEVELS.indexOf(this.level)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const extra = args.map((a) => {
|
|
65
|
+
if (typeof a === "string") return a;
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(a);
|
|
68
|
+
} catch {
|
|
69
|
+
return String(a);
|
|
70
|
+
}
|
|
71
|
+
}).join(" ");
|
|
72
|
+
const full = extra ? `${message} ${extra}` : message;
|
|
73
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
74
|
+
const label = this.format === "minimal" ? level : level.toUpperCase();
|
|
75
|
+
const emoji = this.format === "emoji" ? ` ${EMOJI[level]}` : "";
|
|
76
|
+
const line = `${ts} [${this.name}]${emoji} ${label}: ${full}`;
|
|
77
|
+
const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : level === "debug" ? console.debug : console.log;
|
|
78
|
+
fn(line);
|
|
79
|
+
}
|
|
80
|
+
error = (m, ...a) => this.write("error", m, a);
|
|
81
|
+
warn = (m, ...a) => this.write("warn", m, a);
|
|
82
|
+
info = (m, ...a) => this.write("info", m, a);
|
|
83
|
+
debug = (m, ...a) => this.write("debug", m, a);
|
|
84
|
+
http = (m, ...a) => this.write("http", m, a);
|
|
85
|
+
verbose = (m, ...a) => this.write("verbose", m, a);
|
|
86
|
+
silly = (m, ...a) => this.write("silly", m, a);
|
|
87
|
+
setFormat(format) {
|
|
88
|
+
this.format = format;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var Logger = class {
|
|
92
|
+
static instances = {};
|
|
93
|
+
static currentFormat = "minimal";
|
|
94
|
+
static currentLevel;
|
|
95
|
+
static get(name = "mcp-use") {
|
|
96
|
+
return this.instances[name] ??= new SimpleConsoleLogger(
|
|
97
|
+
name,
|
|
98
|
+
this.currentLevel ?? envLevel(),
|
|
99
|
+
this.currentFormat
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
static configure({
|
|
103
|
+
level = envLevel(),
|
|
104
|
+
format = "minimal"
|
|
105
|
+
} = {}) {
|
|
106
|
+
this.currentLevel = level;
|
|
107
|
+
this.currentFormat = format;
|
|
108
|
+
for (const log of Object.values(this.instances)) {
|
|
109
|
+
log.level = level;
|
|
110
|
+
log.format = format;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
static setDebug(enabled) {
|
|
114
|
+
const level = enabled === 2 || enabled === true ? "debug" : "info";
|
|
115
|
+
this.currentLevel = level;
|
|
116
|
+
for (const log of Object.values(this.instances)) log.level = level;
|
|
117
|
+
try {
|
|
118
|
+
if (typeof process !== "undefined" && process.env) {
|
|
119
|
+
process.env.MCP_USE_LOG_LEVEL = level;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
static setFormat(format) {
|
|
125
|
+
this.configure({ format });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var logger = Logger.get();
|
|
129
|
+
|
|
130
|
+
// src/utils/version.ts
|
|
131
|
+
var VERSION = "2.2.5";
|
|
132
|
+
function getPackageVersion() {
|
|
133
|
+
return VERSION;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/telemetry/events.ts
|
|
137
|
+
var BaseTelemetryEvent = class {
|
|
138
|
+
};
|
|
139
|
+
var MCPAgentExecutionEvent = class extends BaseTelemetryEvent {
|
|
140
|
+
constructor(data) {
|
|
141
|
+
super();
|
|
142
|
+
this.data = data;
|
|
143
|
+
}
|
|
144
|
+
get name() {
|
|
145
|
+
return "mcp_agent_execution";
|
|
146
|
+
}
|
|
147
|
+
get properties() {
|
|
148
|
+
return {
|
|
149
|
+
// Core execution info
|
|
150
|
+
execution_method: this.data.executionMethod,
|
|
151
|
+
query_length: this.data.query.length,
|
|
152
|
+
success: this.data.success,
|
|
153
|
+
// Agent configuration
|
|
154
|
+
model_provider: this.data.modelProvider,
|
|
155
|
+
model_name: this.data.modelName,
|
|
156
|
+
server_count: this.data.serverCount,
|
|
157
|
+
total_tools_available: this.data.totalToolsAvailable,
|
|
158
|
+
max_steps_configured: this.data.maxStepsConfigured,
|
|
159
|
+
memory_enabled: this.data.memoryEnabled,
|
|
160
|
+
use_server_manager: this.data.useServerManager,
|
|
161
|
+
// Execution parameters (always include, even if null)
|
|
162
|
+
max_steps_used: this.data.maxStepsUsed,
|
|
163
|
+
manage_connector: this.data.manageConnector,
|
|
164
|
+
external_history_used: this.data.externalHistoryUsed,
|
|
165
|
+
// Execution results (always include, even if null)
|
|
166
|
+
steps_taken: this.data.stepsTaken ?? null,
|
|
167
|
+
tools_used_count: this.data.toolsUsedCount ?? null,
|
|
168
|
+
response_length: this.data.response ? this.data.response.length : null,
|
|
169
|
+
execution_time_ms: this.data.executionTimeMs ?? null,
|
|
170
|
+
error_type: this.data.errorType ?? null,
|
|
171
|
+
conversation_history_length: this.data.conversationHistoryLength ?? null
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var MCPClientInitEvent = class extends BaseTelemetryEvent {
|
|
176
|
+
constructor(data) {
|
|
177
|
+
super();
|
|
178
|
+
this.data = data;
|
|
179
|
+
}
|
|
180
|
+
get name() {
|
|
181
|
+
return "mcpclient_init";
|
|
182
|
+
}
|
|
183
|
+
get properties() {
|
|
184
|
+
return {
|
|
185
|
+
code_mode: this.data.codeMode,
|
|
186
|
+
sandbox: this.data.sandbox,
|
|
187
|
+
all_callbacks: this.data.allCallbacks,
|
|
188
|
+
verify: this.data.verify,
|
|
189
|
+
servers: this.data.servers,
|
|
190
|
+
num_servers: this.data.numServers,
|
|
191
|
+
is_browser: this.data.isBrowser
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var ConnectorInitEvent = class extends BaseTelemetryEvent {
|
|
196
|
+
constructor(data) {
|
|
197
|
+
super();
|
|
198
|
+
this.data = data;
|
|
199
|
+
}
|
|
200
|
+
get name() {
|
|
201
|
+
return "connector_init";
|
|
202
|
+
}
|
|
203
|
+
get properties() {
|
|
204
|
+
return {
|
|
205
|
+
connector_type: this.data.connectorType,
|
|
206
|
+
server_command: this.data.serverCommand ?? null,
|
|
207
|
+
server_args: this.data.serverArgs ?? null,
|
|
208
|
+
server_url: this.data.serverUrl ?? null,
|
|
209
|
+
public_identifier: this.data.publicIdentifier ?? null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var ClientAddServerEvent = class extends BaseTelemetryEvent {
|
|
214
|
+
constructor(data) {
|
|
215
|
+
super();
|
|
216
|
+
this.data = data;
|
|
217
|
+
}
|
|
218
|
+
get name() {
|
|
219
|
+
return "client_add_server";
|
|
220
|
+
}
|
|
221
|
+
get properties() {
|
|
222
|
+
const { serverName, serverConfig } = this.data;
|
|
223
|
+
const url = serverConfig.url;
|
|
224
|
+
return {
|
|
225
|
+
server_name: serverName,
|
|
226
|
+
server_url_domain: url ? this._extractHostname(url) : null,
|
|
227
|
+
transport: serverConfig.transport ?? null,
|
|
228
|
+
has_auth: !!(serverConfig.authToken || serverConfig.authProvider)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
_extractHostname(url) {
|
|
232
|
+
try {
|
|
233
|
+
return new URL(url).hostname;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var ClientRemoveServerEvent = class extends BaseTelemetryEvent {
|
|
240
|
+
constructor(data) {
|
|
241
|
+
super();
|
|
242
|
+
this.data = data;
|
|
243
|
+
}
|
|
244
|
+
get name() {
|
|
245
|
+
return "client_remove_server";
|
|
246
|
+
}
|
|
247
|
+
get properties() {
|
|
248
|
+
return {
|
|
249
|
+
server_name: this.data.serverName
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/telemetry/tel-fetch.ts
|
|
255
|
+
async function telFetch(url, init) {
|
|
256
|
+
try {
|
|
257
|
+
await fetch(url, init);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
262
|
+
var POSTHOG_API_KEY = "phc_lyTtbYwvkdSbrcMQNPiKiiRWrrM1seyKIMjycSvItEI";
|
|
263
|
+
var CONTENT_PROPERTY = /(^|_)(arguments?|args|body|command|headers?|location|message|query|response|secret|subject|token|uri|url|user_agent)(_|$)/i;
|
|
264
|
+
var IDENTIFYING_PROPERTY = /(^|_)(server_identifiers?|server_names?|servers|tool_names?|tools_(available|used)_names)(_|$)/i;
|
|
265
|
+
var AGGREGATE_PROPERTY = /(_count|_length|_duration(?:_ms)?|_time_ms|(^|_)num_[a-z0-9_]+)$/i;
|
|
266
|
+
function normalizePropertyKey(key) {
|
|
267
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9_$]+/gi, "_").toLowerCase();
|
|
268
|
+
}
|
|
269
|
+
function sanitizeValue(value, seen) {
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
if (seen.has(value)) {
|
|
272
|
+
throw new TypeError("Cyclic telemetry properties are not supported");
|
|
273
|
+
}
|
|
274
|
+
seen.add(value);
|
|
275
|
+
const sanitized = value.map((item) => sanitizeValue(item, seen));
|
|
276
|
+
seen.delete(value);
|
|
277
|
+
return sanitized;
|
|
278
|
+
}
|
|
279
|
+
if (value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
280
|
+
if (seen.has(value)) {
|
|
281
|
+
throw new TypeError("Cyclic telemetry properties are not supported");
|
|
282
|
+
}
|
|
283
|
+
seen.add(value);
|
|
284
|
+
const sanitized = sanitizeProperties(
|
|
285
|
+
value,
|
|
286
|
+
seen
|
|
287
|
+
);
|
|
288
|
+
seen.delete(value);
|
|
289
|
+
return sanitized;
|
|
290
|
+
}
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
function sanitizeProperties(properties, seen = /* @__PURE__ */ new WeakSet()) {
|
|
294
|
+
const sanitized = {};
|
|
295
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
296
|
+
const normalizedKey = normalizePropertyKey(key);
|
|
297
|
+
if (AGGREGATE_PROPERTY.test(normalizedKey)) {
|
|
298
|
+
if (value === null || typeof value === "number") {
|
|
299
|
+
sanitized[key] = value;
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (IDENTIFYING_PROPERTY.test(normalizedKey) || CONTENT_PROPERTY.test(normalizedKey)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
sanitized[key] = sanitizeValue(value, seen);
|
|
307
|
+
}
|
|
308
|
+
return sanitized;
|
|
309
|
+
}
|
|
310
|
+
async function capturePostHog(params) {
|
|
311
|
+
try {
|
|
312
|
+
const host = params.host ?? POSTHOG_HOST;
|
|
313
|
+
const apiKey = params.apiKey ?? POSTHOG_API_KEY;
|
|
314
|
+
const body = JSON.stringify({
|
|
315
|
+
api_key: apiKey,
|
|
316
|
+
event: params.event,
|
|
317
|
+
distinct_id: params.distinctId,
|
|
318
|
+
properties: sanitizeProperties(params.properties),
|
|
319
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
320
|
+
});
|
|
321
|
+
await telFetch(`${host}/i/v0/e/`, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: { "Content-Type": "application/json" },
|
|
324
|
+
keepalive: true,
|
|
325
|
+
body
|
|
326
|
+
});
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/telemetry/telemetry.ts
|
|
332
|
+
function generateUUID() {
|
|
333
|
+
return globalThis.crypto.randomUUID();
|
|
334
|
+
}
|
|
335
|
+
function secureRandomString() {
|
|
336
|
+
const array = new Uint8Array(8);
|
|
337
|
+
globalThis.crypto.getRandomValues(array);
|
|
338
|
+
return Array.from(array, (v) => v.toString(16).padStart(2, "0")).join("");
|
|
339
|
+
}
|
|
340
|
+
var USER_ID_STORAGE_KEY = "mcp_use_user_id";
|
|
341
|
+
var PROJECT_API_KEY = "phc_lyTtbYwvkdSbrcMQNPiKiiRWrrM1seyKIMjycSvItEI";
|
|
342
|
+
var HOST = "https://eu.i.posthog.com";
|
|
343
|
+
var configuredStorage = null;
|
|
344
|
+
function isLocalStorageFunctional() {
|
|
345
|
+
return typeof localStorage !== "undefined" && typeof localStorage.getItem === "function" && typeof localStorage.setItem === "function" && typeof localStorage.removeItem === "function";
|
|
346
|
+
}
|
|
347
|
+
function createLocalStorageBackend() {
|
|
348
|
+
if (!isLocalStorageFunctional()) return null;
|
|
349
|
+
try {
|
|
350
|
+
localStorage.setItem("__mcp_use_test__", "1");
|
|
351
|
+
localStorage.removeItem("__mcp_use_test__");
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
getUserId() {
|
|
357
|
+
try {
|
|
358
|
+
return localStorage.getItem(USER_ID_STORAGE_KEY);
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
setUserId(id) {
|
|
364
|
+
try {
|
|
365
|
+
localStorage.setItem(USER_ID_STORAGE_KEY, id);
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function detectRuntimeEnvironment() {
|
|
372
|
+
try {
|
|
373
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
374
|
+
return "bun";
|
|
375
|
+
}
|
|
376
|
+
if (typeof globalThis.Deno !== "undefined") {
|
|
377
|
+
return "deno";
|
|
378
|
+
}
|
|
379
|
+
if (typeof navigator !== "undefined" && navigator.userAgent?.includes("Cloudflare-Workers")) {
|
|
380
|
+
return "cloudflare-workers";
|
|
381
|
+
}
|
|
382
|
+
if (typeof globalThis.EdgeRuntime !== "undefined") {
|
|
383
|
+
return "edge";
|
|
384
|
+
}
|
|
385
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
386
|
+
return "browser";
|
|
387
|
+
}
|
|
388
|
+
if (typeof process !== "undefined" && typeof process.versions?.node !== "undefined") {
|
|
389
|
+
return "node";
|
|
390
|
+
}
|
|
391
|
+
return "unknown";
|
|
392
|
+
} catch {
|
|
393
|
+
return "unknown";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function readSourceHint() {
|
|
397
|
+
if (typeof process !== "undefined" && process.env?.MCP_USE_TELEMETRY_SOURCE) {
|
|
398
|
+
return process.env.MCP_USE_TELEMETRY_SOURCE;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
if (isLocalStorageFunctional()) {
|
|
402
|
+
return localStorage.getItem("MCP_USE_TELEMETRY_SOURCE") ?? void 0;
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
return void 0;
|
|
407
|
+
}
|
|
408
|
+
function isTelemetryDisabled() {
|
|
409
|
+
if (typeof window !== "undefined" && window.__MCP_USE_ANONYMIZED_TELEMETRY__ === false) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
if (typeof process !== "undefined" && process.env?.MCP_USE_ANONYMIZED_TELEMETRY?.toLowerCase() === "false") {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
if (isLocalStorageFunctional() && localStorage.getItem("MCP_USE_ANONYMIZED_TELEMETRY") === "false") {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
function sessionId() {
|
|
424
|
+
try {
|
|
425
|
+
return `session-${generateUUID()}`;
|
|
426
|
+
} catch {
|
|
427
|
+
return `session-${Date.now()}-${secureRandomString()}`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
var Telemetry = class _Telemetry {
|
|
431
|
+
static instance = null;
|
|
432
|
+
UNKNOWN_USER_ID = "UNKNOWN_USER_ID";
|
|
433
|
+
_currUserId = null;
|
|
434
|
+
_telemetryEnabled = false;
|
|
435
|
+
_pending = /* @__PURE__ */ new Set();
|
|
436
|
+
_runtimeEnvironment;
|
|
437
|
+
_storageCapability;
|
|
438
|
+
_storage;
|
|
439
|
+
_source;
|
|
440
|
+
_productVersion;
|
|
441
|
+
constructor() {
|
|
442
|
+
this._runtimeEnvironment = detectRuntimeEnvironment();
|
|
443
|
+
this._storage = configuredStorage ?? createLocalStorageBackend() ?? null;
|
|
444
|
+
this._storageCapability = this._storage ? "persistent" : "session-only";
|
|
445
|
+
this._source = readSourceHint() || this._runtimeEnvironment;
|
|
446
|
+
const disabled = isTelemetryDisabled();
|
|
447
|
+
const canSupport = this._runtimeEnvironment !== "unknown";
|
|
448
|
+
if (disabled) {
|
|
449
|
+
this._telemetryEnabled = false;
|
|
450
|
+
logger.debug("Telemetry disabled via opt-out");
|
|
451
|
+
} else if (!canSupport) {
|
|
452
|
+
this._telemetryEnabled = false;
|
|
453
|
+
logger.debug(
|
|
454
|
+
`Telemetry disabled - unknown environment: ${this._runtimeEnvironment}`
|
|
455
|
+
);
|
|
456
|
+
} else {
|
|
457
|
+
logger.debug(
|
|
458
|
+
"Anonymized telemetry enabled. Set MCP_USE_ANONYMIZED_TELEMETRY=false to disable."
|
|
459
|
+
);
|
|
460
|
+
this._telemetryEnabled = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
get runtimeEnvironment() {
|
|
464
|
+
return this._runtimeEnvironment;
|
|
465
|
+
}
|
|
466
|
+
get storageCapability() {
|
|
467
|
+
return this._storageCapability;
|
|
468
|
+
}
|
|
469
|
+
static getInstance() {
|
|
470
|
+
if (!_Telemetry.instance) {
|
|
471
|
+
_Telemetry.instance = new _Telemetry();
|
|
472
|
+
}
|
|
473
|
+
return _Telemetry.instance;
|
|
474
|
+
}
|
|
475
|
+
setSource(source) {
|
|
476
|
+
this._source = source;
|
|
477
|
+
try {
|
|
478
|
+
if (isLocalStorageFunctional()) {
|
|
479
|
+
localStorage.setItem("MCP_USE_TELEMETRY_SOURCE", source);
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
logger.debug(`Telemetry source set to: ${source}`);
|
|
484
|
+
}
|
|
485
|
+
getSource() {
|
|
486
|
+
return this._source;
|
|
487
|
+
}
|
|
488
|
+
setProductVersion(version) {
|
|
489
|
+
this._productVersion = version;
|
|
490
|
+
}
|
|
491
|
+
get isEnabled() {
|
|
492
|
+
return this._telemetryEnabled;
|
|
493
|
+
}
|
|
494
|
+
get userId() {
|
|
495
|
+
if (this._currUserId) return this._currUserId;
|
|
496
|
+
try {
|
|
497
|
+
if (this._storage) {
|
|
498
|
+
const existing = this._storage.getUserId();
|
|
499
|
+
if (existing) {
|
|
500
|
+
this._currUserId = existing;
|
|
501
|
+
return existing;
|
|
502
|
+
}
|
|
503
|
+
const id = generateUUID();
|
|
504
|
+
this._storage.setUserId(id);
|
|
505
|
+
this._currUserId = id;
|
|
506
|
+
return id;
|
|
507
|
+
}
|
|
508
|
+
this._currUserId = sessionId();
|
|
509
|
+
} catch {
|
|
510
|
+
this._currUserId = this.UNKNOWN_USER_ID;
|
|
511
|
+
}
|
|
512
|
+
return this._currUserId;
|
|
513
|
+
}
|
|
514
|
+
async capture(event) {
|
|
515
|
+
if (!this._telemetryEnabled) return;
|
|
516
|
+
const currentUserId = this.userId;
|
|
517
|
+
const properties = {
|
|
518
|
+
...event.properties,
|
|
519
|
+
mcp_use_version: this._productVersion ?? getPackageVersion(),
|
|
520
|
+
language: "typescript",
|
|
521
|
+
source: this._source,
|
|
522
|
+
runtime: this._runtimeEnvironment
|
|
523
|
+
};
|
|
524
|
+
const p = capturePostHog({
|
|
525
|
+
host: HOST,
|
|
526
|
+
apiKey: PROJECT_API_KEY,
|
|
527
|
+
event: event.name,
|
|
528
|
+
distinctId: currentUserId,
|
|
529
|
+
properties
|
|
530
|
+
});
|
|
531
|
+
this._pending.add(p);
|
|
532
|
+
void p.finally(() => this._pending.delete(p));
|
|
533
|
+
}
|
|
534
|
+
async trackAgentExecution(data) {
|
|
535
|
+
if (!this.isEnabled) return;
|
|
536
|
+
await this.capture(new MCPAgentExecutionEvent(data));
|
|
537
|
+
}
|
|
538
|
+
async trackMCPClientInit(data) {
|
|
539
|
+
if (!this.isEnabled) return;
|
|
540
|
+
await this.capture(new MCPClientInitEvent(data));
|
|
541
|
+
}
|
|
542
|
+
async trackConnectorInit(data) {
|
|
543
|
+
if (!this.isEnabled) return;
|
|
544
|
+
await this.capture(new ConnectorInitEvent(data));
|
|
545
|
+
}
|
|
546
|
+
async trackClientAddServer(serverName, serverConfig) {
|
|
547
|
+
if (!this.isEnabled) return;
|
|
548
|
+
await this.capture(new ClientAddServerEvent({ serverName, serverConfig }));
|
|
549
|
+
}
|
|
550
|
+
async trackClientRemoveServer(serverName) {
|
|
551
|
+
if (!this.isEnabled) return;
|
|
552
|
+
await this.capture(new ClientRemoveServerEvent({ serverName }));
|
|
553
|
+
}
|
|
554
|
+
async trackUseMcpConnection(data) {
|
|
555
|
+
if (!this.isEnabled) return;
|
|
556
|
+
await this.capture({
|
|
557
|
+
name: "usemcp_connection",
|
|
558
|
+
properties: {
|
|
559
|
+
url_domain: new URL(data.url).hostname,
|
|
560
|
+
transport_type: data.transportType,
|
|
561
|
+
success: data.success,
|
|
562
|
+
error_type: data.errorType ?? null,
|
|
563
|
+
connection_time_ms: data.connectionTimeMs ?? null,
|
|
564
|
+
has_oauth: data.hasOAuth,
|
|
565
|
+
has_sampling: data.hasSampling,
|
|
566
|
+
has_elicitation: data.hasElicitation
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
async trackUseMcpToolCall(data) {
|
|
571
|
+
if (!this.isEnabled) return;
|
|
572
|
+
await this.capture({
|
|
573
|
+
name: "usemcp_tool_call",
|
|
574
|
+
properties: {
|
|
575
|
+
tool_name: data.toolName,
|
|
576
|
+
success: data.success,
|
|
577
|
+
error_type: data.errorType ?? null,
|
|
578
|
+
execution_time_ms: data.executionTimeMs ?? null
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
async trackUseMcpResourceRead(data) {
|
|
583
|
+
if (!this.isEnabled) return;
|
|
584
|
+
await this.capture({
|
|
585
|
+
name: "usemcp_resource_read",
|
|
586
|
+
properties: {
|
|
587
|
+
resource_uri_scheme: data.resourceUri.split(":")[0],
|
|
588
|
+
success: data.success,
|
|
589
|
+
error_type: data.errorType ?? null
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
identify(userId, properties) {
|
|
594
|
+
this._currUserId = userId;
|
|
595
|
+
this._storage?.setUserId(userId);
|
|
596
|
+
if (this._telemetryEnabled) {
|
|
597
|
+
void capturePostHog({
|
|
598
|
+
host: HOST,
|
|
599
|
+
apiKey: PROJECT_API_KEY,
|
|
600
|
+
event: "$identify",
|
|
601
|
+
distinctId: userId,
|
|
602
|
+
properties: { $set: properties ?? {} }
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
reset() {
|
|
607
|
+
this._currUserId = null;
|
|
608
|
+
}
|
|
609
|
+
flush() {
|
|
610
|
+
void Promise.allSettled([...this._pending]);
|
|
611
|
+
}
|
|
612
|
+
async shutdown() {
|
|
613
|
+
try {
|
|
614
|
+
await Promise.allSettled([...this._pending]);
|
|
615
|
+
logger.debug("Telemetry fetch captures flushed");
|
|
616
|
+
} catch (e) {
|
|
617
|
+
logger.debug(`Error flushing telemetry captures: ${e}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
var Tel = Telemetry;
|
|
622
|
+
function setTelemetrySource(source) {
|
|
623
|
+
Tel.getInstance().setSource(source);
|
|
624
|
+
}
|
|
625
|
+
function setProductVersion(version) {
|
|
626
|
+
Tel.getInstance().setProductVersion(version);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/telemetry/configure-browser.ts
|
|
630
|
+
setClientTelemetryTracker({
|
|
631
|
+
addServer: (name, config) => Telemetry.getInstance().trackClientAddServer(name, config).catch(() => void 0),
|
|
632
|
+
removeServer: (name) => Telemetry.getInstance().trackClientRemoveServer(name).catch(() => void 0)
|
|
633
|
+
});
|
|
634
|
+
setConnectorTelemetryTracker(
|
|
635
|
+
(data) => Telemetry.getInstance().trackConnectorInit(data).catch(() => void 0)
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
// src/auth/browser.ts
|
|
639
|
+
import {
|
|
640
|
+
extractWWWAuthenticateParams
|
|
641
|
+
} from "@modelcontextprotocol/client";
|
|
642
|
+
|
|
643
|
+
// src/auth/storage.ts
|
|
644
|
+
var AUTH_CRYPTO_DATABASE = "mcp-use-oauth-crypto";
|
|
645
|
+
var AUTH_CRYPTO_STORE = "keys";
|
|
646
|
+
var AUTH_CRYPTO_KEY = "aes-gcm-v1";
|
|
647
|
+
var textEncoder = new TextEncoder();
|
|
648
|
+
var textDecoder = new TextDecoder();
|
|
649
|
+
var LocalStorageKVStore = class {
|
|
650
|
+
fallback = /* @__PURE__ */ new Map();
|
|
651
|
+
keyPromise;
|
|
652
|
+
durable = true;
|
|
653
|
+
async get(key) {
|
|
654
|
+
if (!this.durable) return this.fallback.get(key) ?? null;
|
|
655
|
+
let stored;
|
|
656
|
+
try {
|
|
657
|
+
stored = localStorage.getItem(key);
|
|
658
|
+
} catch {
|
|
659
|
+
this.durable = false;
|
|
660
|
+
return this.fallback.get(key) ?? null;
|
|
661
|
+
}
|
|
662
|
+
if (stored === null) return null;
|
|
663
|
+
const envelope = parseEncryptedEnvelope(stored);
|
|
664
|
+
if (!envelope) {
|
|
665
|
+
await this.set(key, stored);
|
|
666
|
+
return stored;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const cryptoKey = await this.getCryptoKey();
|
|
670
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
671
|
+
{
|
|
672
|
+
name: "AES-GCM",
|
|
673
|
+
iv: decodeBase64(envelope.iv),
|
|
674
|
+
additionalData: textEncoder.encode(key)
|
|
675
|
+
},
|
|
676
|
+
cryptoKey,
|
|
677
|
+
decodeBase64(envelope.ciphertext)
|
|
678
|
+
);
|
|
679
|
+
return textDecoder.decode(plaintext);
|
|
680
|
+
} catch {
|
|
681
|
+
await this.remove(key);
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async set(key, value) {
|
|
686
|
+
if (!this.durable) {
|
|
687
|
+
this.fallback.set(key, value);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const cryptoKey = await this.getCryptoKey();
|
|
692
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
693
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
694
|
+
{
|
|
695
|
+
name: "AES-GCM",
|
|
696
|
+
iv,
|
|
697
|
+
additionalData: textEncoder.encode(key)
|
|
698
|
+
},
|
|
699
|
+
cryptoKey,
|
|
700
|
+
textEncoder.encode(value)
|
|
701
|
+
);
|
|
702
|
+
const envelope = {
|
|
703
|
+
v: 1,
|
|
704
|
+
alg: "A256GCM",
|
|
705
|
+
iv: encodeBase64(iv),
|
|
706
|
+
ciphertext: encodeBase64(new Uint8Array(ciphertext))
|
|
707
|
+
};
|
|
708
|
+
localStorage.setItem(key, JSON.stringify(envelope));
|
|
709
|
+
this.fallback.delete(key);
|
|
710
|
+
} catch {
|
|
711
|
+
this.durable = false;
|
|
712
|
+
try {
|
|
713
|
+
localStorage.removeItem(key);
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
this.fallback.set(key, value);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
remove(key) {
|
|
720
|
+
this.fallback.delete(key);
|
|
721
|
+
try {
|
|
722
|
+
localStorage.removeItem(key);
|
|
723
|
+
} catch {
|
|
724
|
+
this.durable = false;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
keys() {
|
|
728
|
+
const out = new Set(this.fallback.keys());
|
|
729
|
+
if (this.durable) {
|
|
730
|
+
try {
|
|
731
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
732
|
+
const key = localStorage.key(i);
|
|
733
|
+
if (key) out.add(key);
|
|
734
|
+
}
|
|
735
|
+
} catch {
|
|
736
|
+
this.durable = false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return [...out];
|
|
740
|
+
}
|
|
741
|
+
getCryptoKey() {
|
|
742
|
+
this.keyPromise ??= getOrCreateCryptoKey();
|
|
743
|
+
return this.keyPromise;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function parseEncryptedEnvelope(value) {
|
|
747
|
+
try {
|
|
748
|
+
const parsed = JSON.parse(value);
|
|
749
|
+
if (!parsed || typeof parsed !== "object" || !("v" in parsed) || parsed.v !== 1 || !("alg" in parsed) || parsed.alg !== "A256GCM" || !("iv" in parsed) || typeof parsed.iv !== "string" || !("ciphertext" in parsed) || typeof parsed.ciphertext !== "string") {
|
|
750
|
+
return void 0;
|
|
751
|
+
}
|
|
752
|
+
return parsed;
|
|
753
|
+
} catch {
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function getOrCreateCryptoKey() {
|
|
758
|
+
if (!globalThis.crypto?.subtle || typeof indexedDB === "undefined") {
|
|
759
|
+
throw new Error("Durable browser cryptography is unavailable");
|
|
760
|
+
}
|
|
761
|
+
const candidate = await globalThis.crypto.subtle.generateKey(
|
|
762
|
+
{ name: "AES-GCM", length: 256 },
|
|
763
|
+
false,
|
|
764
|
+
["encrypt", "decrypt"]
|
|
765
|
+
);
|
|
766
|
+
const database = await openCryptoDatabase();
|
|
767
|
+
try {
|
|
768
|
+
return await new Promise((resolve, reject) => {
|
|
769
|
+
const transaction = database.transaction(AUTH_CRYPTO_STORE, "readwrite");
|
|
770
|
+
const store = transaction.objectStore(AUTH_CRYPTO_STORE);
|
|
771
|
+
const request = store.get(AUTH_CRYPTO_KEY);
|
|
772
|
+
let selected;
|
|
773
|
+
request.onsuccess = () => {
|
|
774
|
+
selected = request.result;
|
|
775
|
+
if (!selected) {
|
|
776
|
+
selected = candidate;
|
|
777
|
+
store.put(candidate, AUTH_CRYPTO_KEY);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
request.onerror = () => reject(request.error);
|
|
781
|
+
transaction.oncomplete = () => {
|
|
782
|
+
if (selected) resolve(selected);
|
|
783
|
+
else reject(new Error("OAuth encryption key was not initialized"));
|
|
784
|
+
};
|
|
785
|
+
transaction.onerror = () => reject(transaction.error);
|
|
786
|
+
transaction.onabort = () => reject(transaction.error);
|
|
787
|
+
});
|
|
788
|
+
} finally {
|
|
789
|
+
database.close();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function openCryptoDatabase() {
|
|
793
|
+
return new Promise((resolve, reject) => {
|
|
794
|
+
const request = indexedDB.open(AUTH_CRYPTO_DATABASE, 1);
|
|
795
|
+
request.onupgradeneeded = () => {
|
|
796
|
+
const database = request.result;
|
|
797
|
+
if (!database.objectStoreNames.contains(AUTH_CRYPTO_STORE)) {
|
|
798
|
+
database.createObjectStore(AUTH_CRYPTO_STORE);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
request.onsuccess = () => resolve(request.result);
|
|
802
|
+
request.onerror = () => reject(request.error);
|
|
803
|
+
request.onblocked = () => reject(new Error("OAuth encryption database is blocked"));
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function encodeBase64(bytes) {
|
|
807
|
+
let binary = "";
|
|
808
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
809
|
+
return btoa(binary);
|
|
810
|
+
}
|
|
811
|
+
function decodeBase64(value) {
|
|
812
|
+
const binary = atob(value);
|
|
813
|
+
const bytes = new Uint8Array(binary.length);
|
|
814
|
+
for (let index = 0; index < binary.length; index++) {
|
|
815
|
+
bytes[index] = binary.charCodeAt(index);
|
|
816
|
+
}
|
|
817
|
+
return bytes;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/auth/session-store.ts
|
|
821
|
+
import { validateClientMetadataUrl } from "@modelcontextprotocol/client";
|
|
822
|
+
|
|
823
|
+
// src/auth/url.ts
|
|
824
|
+
function sanitizeUrl(raw) {
|
|
825
|
+
const abort = () => {
|
|
826
|
+
throw new Error(`Invalid url to pass to open(): ${raw}`);
|
|
827
|
+
};
|
|
828
|
+
let url;
|
|
829
|
+
try {
|
|
830
|
+
url = new URL(raw);
|
|
831
|
+
} catch (_) {
|
|
832
|
+
abort();
|
|
833
|
+
}
|
|
834
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") abort();
|
|
835
|
+
if (url.hostname !== encodeURIComponent(url.hostname)) abort();
|
|
836
|
+
if (url.username) url.username = encodeURIComponent(url.username);
|
|
837
|
+
if (url.password) url.password = encodeURIComponent(url.password);
|
|
838
|
+
url.pathname = url.pathname.slice(0, 1) + encodeURIComponent(url.pathname.slice(1)).replace(/%2f/gi, "/");
|
|
839
|
+
url.search = url.search.slice(0, 1) + Array.from(url.searchParams.entries()).map(sanitizeParam).join("&");
|
|
840
|
+
url.hash = url.hash.slice(0, 1) + encodeURIComponent(url.hash.slice(1));
|
|
841
|
+
return url.href;
|
|
842
|
+
}
|
|
843
|
+
function sanitizeParam([k, v]) {
|
|
844
|
+
return `${encodeURIComponent(k)}${v.length > 0 ? `=${encodeURIComponent(v)}` : ""}`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/auth/session-store.ts
|
|
848
|
+
var OAuthSessionStore = class _OAuthSessionStore {
|
|
849
|
+
serverUrl;
|
|
850
|
+
storageKeyPrefix;
|
|
851
|
+
serverUrlHash;
|
|
852
|
+
clientName;
|
|
853
|
+
clientUri;
|
|
854
|
+
logoUri;
|
|
855
|
+
callbackUrl;
|
|
856
|
+
clientMetadataUrl;
|
|
857
|
+
scope;
|
|
858
|
+
store;
|
|
859
|
+
allowClientSecret;
|
|
860
|
+
constructor(serverUrl, options, store) {
|
|
861
|
+
validateClientMetadataUrl(options.clientMetadataUrl);
|
|
862
|
+
this.serverUrl = serverUrl;
|
|
863
|
+
this.storageKeyPrefix = options.storageKeyPrefix || "mcp:auth";
|
|
864
|
+
this.serverUrlHash = _OAuthSessionStore.hashString(serverUrl);
|
|
865
|
+
this.clientName = options.clientName || "mcp-use";
|
|
866
|
+
this.clientUri = options.clientUri || (typeof window !== "undefined" ? window.location.origin : "https://mcp-use.com");
|
|
867
|
+
this.logoUri = options.logoUri || "https://mcp-use.com/logo.png";
|
|
868
|
+
this.callbackUrl = sanitizeUrl(
|
|
869
|
+
options.callbackUrl || (typeof window !== "undefined" ? new URL("/oauth/callback", window.location.origin).toString() : "/oauth/callback")
|
|
870
|
+
);
|
|
871
|
+
this.clientMetadataUrl = options.clientMetadataUrl;
|
|
872
|
+
this.scope = options.scope;
|
|
873
|
+
this.store = store;
|
|
874
|
+
this.allowClientSecret = options.allowClientSecret ?? true;
|
|
875
|
+
}
|
|
876
|
+
getKey(keySuffix) {
|
|
877
|
+
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`;
|
|
878
|
+
}
|
|
879
|
+
static hashString(str) {
|
|
880
|
+
let hash = 0;
|
|
881
|
+
for (let i = 0; i < str.length; i++) {
|
|
882
|
+
const char = str.charCodeAt(i);
|
|
883
|
+
hash = (hash << 5) - hash + char;
|
|
884
|
+
hash = hash & hash;
|
|
885
|
+
}
|
|
886
|
+
return Math.abs(hash).toString(16);
|
|
887
|
+
}
|
|
888
|
+
// --- SDK Interface Methods (delegated) ---
|
|
889
|
+
get redirectUrl() {
|
|
890
|
+
return this.callbackUrl;
|
|
891
|
+
}
|
|
892
|
+
get clientMetadata() {
|
|
893
|
+
return {
|
|
894
|
+
redirect_uris: [this.redirectUrl],
|
|
895
|
+
token_endpoint_auth_method: "none",
|
|
896
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
897
|
+
response_types: ["code"],
|
|
898
|
+
client_name: this.clientName,
|
|
899
|
+
client_uri: this.clientUri,
|
|
900
|
+
logo_uri: this.logoUri,
|
|
901
|
+
...this.scope ? { scope: this.scope } : {}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
credentialKey(kind, ctx) {
|
|
905
|
+
return ctx ? this.getKey(`${kind}_${encodeURIComponent(ctx.issuer)}`) : this.getKey(kind);
|
|
906
|
+
}
|
|
907
|
+
async readCredential(kind, ctx) {
|
|
908
|
+
const key = this.credentialKey(kind, ctx);
|
|
909
|
+
const data = await this.store.get(key);
|
|
910
|
+
if (!data && ctx) {
|
|
911
|
+
const legacyKey = this.credentialKey(kind);
|
|
912
|
+
const legacyData = await this.store.get(legacyKey);
|
|
913
|
+
if (legacyData) {
|
|
914
|
+
try {
|
|
915
|
+
const legacyValue = JSON.parse(legacyData);
|
|
916
|
+
if (!legacyValue.issuer || legacyValue.issuer === ctx.issuer) {
|
|
917
|
+
const migratedValue = {
|
|
918
|
+
...legacyValue,
|
|
919
|
+
issuer: ctx.issuer
|
|
920
|
+
};
|
|
921
|
+
const migratedData = JSON.stringify(migratedValue);
|
|
922
|
+
await this.store.set(key, migratedData);
|
|
923
|
+
await this.store.set(legacyKey, migratedData);
|
|
924
|
+
return { key, value: migratedValue };
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
await this.store.remove(legacyKey);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return void 0;
|
|
931
|
+
}
|
|
932
|
+
if (!data) return void 0;
|
|
933
|
+
try {
|
|
934
|
+
return { key, value: JSON.parse(data) };
|
|
935
|
+
} catch (e) {
|
|
936
|
+
console.warn(
|
|
937
|
+
`[${this.storageKeyPrefix}] Failed to parse ${kind.replace("_", " ")}:`,
|
|
938
|
+
e
|
|
939
|
+
);
|
|
940
|
+
await this.store.remove(key);
|
|
941
|
+
return void 0;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async tokens(ctx) {
|
|
945
|
+
return (await this.readCredential("tokens", ctx))?.value;
|
|
946
|
+
}
|
|
947
|
+
async saveTokens(tokens, ctx) {
|
|
948
|
+
const serialized = JSON.stringify(tokens);
|
|
949
|
+
await this.store.set(this.credentialKey("tokens", ctx), serialized);
|
|
950
|
+
if (ctx) await this.store.set(this.credentialKey("tokens"), serialized);
|
|
951
|
+
await this.store.remove(this.getKey("code_verifier"));
|
|
952
|
+
await this.store.remove(this.getKey("last_auth_url"));
|
|
953
|
+
await this.store.remove(this.getKey("last_auth_callback_url"));
|
|
954
|
+
}
|
|
955
|
+
async clientInformation(ctx) {
|
|
956
|
+
if (!this.allowClientSecret) {
|
|
957
|
+
const registeredRedirectUri = await this.store.get(
|
|
958
|
+
this.getKey("client_info_redirect_uri")
|
|
959
|
+
);
|
|
960
|
+
if (registeredRedirectUri !== this.redirectUrl) {
|
|
961
|
+
await this.invalidateCredentials("registration");
|
|
962
|
+
console.info(
|
|
963
|
+
`[${this.storageKeyPrefix}] Re-registering browser OAuth client after its Inspector callback changed or could not be verified.`
|
|
964
|
+
);
|
|
965
|
+
return void 0;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const stored = await this.readCredential("client_info", ctx);
|
|
969
|
+
if (!stored) return void 0;
|
|
970
|
+
const { key, value: clientInfo } = stored;
|
|
971
|
+
try {
|
|
972
|
+
if (!this.allowClientSecret && clientInfo.client_secret) {
|
|
973
|
+
await this.invalidateCredentials("registration");
|
|
974
|
+
console.warn(
|
|
975
|
+
`[${this.storageKeyPrefix}] Recovered stale browser OAuth credentials containing a client_secret.`
|
|
976
|
+
);
|
|
977
|
+
return void 0;
|
|
978
|
+
}
|
|
979
|
+
const storedRedirectUris = Array.isArray(clientInfo.redirect_uris) ? clientInfo.redirect_uris : [];
|
|
980
|
+
const hasMatchingRedirect = storedRedirectUris.length === 0 && this.allowClientSecret || storedRedirectUris.includes(this.redirectUrl);
|
|
981
|
+
if (!hasMatchingRedirect) {
|
|
982
|
+
console.info(
|
|
983
|
+
`[${this.storageKeyPrefix}] Recovering cached OAuth credentials after a redirect URI change.`
|
|
984
|
+
);
|
|
985
|
+
await this.invalidateCredentials("registration");
|
|
986
|
+
return void 0;
|
|
987
|
+
}
|
|
988
|
+
return clientInfo;
|
|
989
|
+
} catch {
|
|
990
|
+
await this.store.remove(key);
|
|
991
|
+
return void 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async saveClientInformation(clientInformation, ctx) {
|
|
995
|
+
const info = clientInformation;
|
|
996
|
+
if (!this.allowClientSecret && info.client_secret) {
|
|
997
|
+
await this.store.remove(this.credentialKey("client_info", ctx));
|
|
998
|
+
if (ctx) await this.store.remove(this.credentialKey("client_info"));
|
|
999
|
+
throw new Error(
|
|
1000
|
+
"Browser OAuth clients must be public clients; client_secret persistence is not allowed."
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const persistedClientInformation = !this.allowClientSecret && (!("redirect_uris" in clientInformation) || !Array.isArray(
|
|
1004
|
+
clientInformation.redirect_uris
|
|
1005
|
+
) || clientInformation.redirect_uris.length === 0) ? { ...clientInformation, redirect_uris: [this.redirectUrl] } : clientInformation;
|
|
1006
|
+
const serialized = JSON.stringify(persistedClientInformation);
|
|
1007
|
+
await this.store.set(this.credentialKey("client_info", ctx), serialized);
|
|
1008
|
+
if (ctx) {
|
|
1009
|
+
await this.store.set(this.credentialKey("client_info"), serialized);
|
|
1010
|
+
}
|
|
1011
|
+
if (!this.allowClientSecret) {
|
|
1012
|
+
await this.store.set(
|
|
1013
|
+
this.getKey("client_info_redirect_uri"),
|
|
1014
|
+
this.redirectUrl
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async saveCodeVerifier(codeVerifier) {
|
|
1019
|
+
await this.store.set(this.getKey("code_verifier"), codeVerifier);
|
|
1020
|
+
}
|
|
1021
|
+
async codeVerifier() {
|
|
1022
|
+
const key = this.getKey("code_verifier");
|
|
1023
|
+
const verifier = await this.store.get(key);
|
|
1024
|
+
if (!verifier) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return verifier;
|
|
1030
|
+
}
|
|
1031
|
+
async invalidateCredentials(scope) {
|
|
1032
|
+
const removeCredentialKeys = async (kind) => {
|
|
1033
|
+
const prefix = `${this.getKey(kind)}_`;
|
|
1034
|
+
for (const key of await this.store.keys()) {
|
|
1035
|
+
if (key === this.getKey(kind) || key.startsWith(prefix)) {
|
|
1036
|
+
await this.store.remove(key);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
switch (scope) {
|
|
1041
|
+
case "registration":
|
|
1042
|
+
await removeCredentialKeys("tokens");
|
|
1043
|
+
await removeCredentialKeys("client_info");
|
|
1044
|
+
await this.store.remove(this.getKey("code_verifier"));
|
|
1045
|
+
await this.store.remove(this.getKey("last_auth_url"));
|
|
1046
|
+
await this.store.remove(this.getKey("last_auth_callback_url"));
|
|
1047
|
+
await this.store.remove(this.getKey("client_info_redirect_uri"));
|
|
1048
|
+
await this.store.remove(this.getKey("token_endpoint"));
|
|
1049
|
+
break;
|
|
1050
|
+
case "all":
|
|
1051
|
+
await removeCredentialKeys("tokens");
|
|
1052
|
+
await removeCredentialKeys("client_info");
|
|
1053
|
+
await this.store.remove(this.getKey("code_verifier"));
|
|
1054
|
+
await this.store.remove(this.getKey("last_auth_url"));
|
|
1055
|
+
await this.store.remove(this.getKey("last_auth_callback_url"));
|
|
1056
|
+
await this.store.remove(this.getKey("client_info_redirect_uri"));
|
|
1057
|
+
await this.store.remove(this.getKey("discovery_state"));
|
|
1058
|
+
await this.store.remove(this.getKey("token_endpoint"));
|
|
1059
|
+
break;
|
|
1060
|
+
case "client":
|
|
1061
|
+
await removeCredentialKeys("client_info");
|
|
1062
|
+
break;
|
|
1063
|
+
case "tokens":
|
|
1064
|
+
await removeCredentialKeys("tokens");
|
|
1065
|
+
break;
|
|
1066
|
+
case "verifier":
|
|
1067
|
+
await this.store.remove(this.getKey("code_verifier"));
|
|
1068
|
+
break;
|
|
1069
|
+
case "discovery":
|
|
1070
|
+
await this.store.remove(this.getKey("discovery_state"));
|
|
1071
|
+
break;
|
|
1072
|
+
default:
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Persist the OAuth discovery state (authorization-server metadata resolved
|
|
1078
|
+
* during the auth flow). Stored with the same durability as the code
|
|
1079
|
+
* verifier so the callback leg can verify it is exchanging the code at the
|
|
1080
|
+
* same authorization server the redirect targeted (SEP-2352 mix-up defense).
|
|
1081
|
+
*/
|
|
1082
|
+
async saveDiscoveryState(state) {
|
|
1083
|
+
await this.store.set(this.getKey("discovery_state"), JSON.stringify(state));
|
|
1084
|
+
}
|
|
1085
|
+
/** Return the previously saved discovery state, or `undefined`. */
|
|
1086
|
+
async discoveryState() {
|
|
1087
|
+
const data = await this.store.get(this.getKey("discovery_state"));
|
|
1088
|
+
if (!data) return void 0;
|
|
1089
|
+
try {
|
|
1090
|
+
return JSON.parse(data);
|
|
1091
|
+
} catch {
|
|
1092
|
+
await this.store.remove(this.getKey("discovery_state"));
|
|
1093
|
+
return void 0;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// --- Helper / non-SDK methods ---
|
|
1097
|
+
/**
|
|
1098
|
+
* Generates and persists `StoredState` for an authorization request,
|
|
1099
|
+
* appends the `state` query param to the URL, and persists the sanitized
|
|
1100
|
+
* URL to `last_auth_url` so it can be replayed on popup-blocker fallback.
|
|
1101
|
+
*
|
|
1102
|
+
* @returns The sanitized authorization URL string with the `state` param appended.
|
|
1103
|
+
*/
|
|
1104
|
+
async storeAuthorizationState(authorizationUrl, opts = {}) {
|
|
1105
|
+
const state = globalThis.crypto.randomUUID();
|
|
1106
|
+
const stateKey = `${this.storageKeyPrefix}_${this.serverUrlHash}_state_${state}`;
|
|
1107
|
+
const stateData = {
|
|
1108
|
+
serverUrlHash: this.serverUrlHash,
|
|
1109
|
+
expiry: Date.now() + 1e3 * 60 * 10,
|
|
1110
|
+
// State expires in 10 minutes
|
|
1111
|
+
providerOptions: {
|
|
1112
|
+
serverUrl: this.serverUrl,
|
|
1113
|
+
storageKeyPrefix: this.storageKeyPrefix,
|
|
1114
|
+
clientName: this.clientName,
|
|
1115
|
+
clientUri: this.clientUri,
|
|
1116
|
+
callbackUrl: this.callbackUrl,
|
|
1117
|
+
...this.clientMetadataUrl ? { clientMetadataUrl: this.clientMetadataUrl } : {},
|
|
1118
|
+
...opts.extraProviderOptions ?? {}
|
|
1119
|
+
},
|
|
1120
|
+
flowType: opts.flowType,
|
|
1121
|
+
returnUrl: opts.returnUrl
|
|
1122
|
+
};
|
|
1123
|
+
authorizationUrl.searchParams.set("state", state);
|
|
1124
|
+
const sanitizedAuthUrl = sanitizeUrl(authorizationUrl.toString());
|
|
1125
|
+
await this.store.set(stateKey, JSON.stringify(stateData));
|
|
1126
|
+
await this.store.set(
|
|
1127
|
+
this.getKey("last_auth_callback_url"),
|
|
1128
|
+
this.redirectUrl
|
|
1129
|
+
);
|
|
1130
|
+
await this.store.set(this.getKey("last_auth_url"), sanitizedAuthUrl);
|
|
1131
|
+
return sanitizedAuthUrl;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Return the token endpoint from SDK-managed discovery state. The SDK
|
|
1135
|
+
* persists this state during `auth()`, avoiding a second discovery flow.
|
|
1136
|
+
*/
|
|
1137
|
+
async getTokenEndpoint() {
|
|
1138
|
+
return (await this.discoveryState())?.authorizationServerMetadata?.token_endpoint ?? null;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Return the protected-resource URL selected during OAuth discovery.
|
|
1142
|
+
* Consumers can persist it and reuse it for server-side refresh exchanges.
|
|
1143
|
+
*/
|
|
1144
|
+
async getResource() {
|
|
1145
|
+
const resource = (await this.discoveryState())?.resourceMetadata?.resource;
|
|
1146
|
+
return typeof resource === "string" ? resource : null;
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// src/auth/browser.ts
|
|
1151
|
+
async function serializeBody(body) {
|
|
1152
|
+
if (typeof body === "string") return body;
|
|
1153
|
+
if (body instanceof URLSearchParams || body instanceof FormData) {
|
|
1154
|
+
return Object.fromEntries(body.entries());
|
|
1155
|
+
}
|
|
1156
|
+
if (body instanceof Blob) return await body.text();
|
|
1157
|
+
return body;
|
|
1158
|
+
}
|
|
1159
|
+
function trimTrailingSlashes(value) {
|
|
1160
|
+
let end = value.length;
|
|
1161
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
1162
|
+
end--;
|
|
1163
|
+
}
|
|
1164
|
+
return value.slice(0, end);
|
|
1165
|
+
}
|
|
1166
|
+
var BrowserOAuthClientProvider = class {
|
|
1167
|
+
/** Protected MCP server URL associated with this provider. */
|
|
1168
|
+
serverUrl;
|
|
1169
|
+
/** Pre-registered public client information, when configured. */
|
|
1170
|
+
staticClientInfo;
|
|
1171
|
+
session;
|
|
1172
|
+
storage;
|
|
1173
|
+
// Browser-only state
|
|
1174
|
+
/** Whether initial connection waits for explicit authentication. */
|
|
1175
|
+
preventAutoAuth;
|
|
1176
|
+
useRedirectFlow;
|
|
1177
|
+
oauthProxyUrl;
|
|
1178
|
+
connectionUrl;
|
|
1179
|
+
proxyOAuthRequests;
|
|
1180
|
+
lastAttemptedAuthUrl = null;
|
|
1181
|
+
authorizationPending = false;
|
|
1182
|
+
/** Latest protected-resource metadata URL advertised by an MCP 401. */
|
|
1183
|
+
challengedResourceMetadataUrl;
|
|
1184
|
+
/** Callback invoked immediately before an authorization popup opens. */
|
|
1185
|
+
onPopupWindow;
|
|
1186
|
+
constructor(serverUrl, options = {}) {
|
|
1187
|
+
if (options.staticClientInfo?.client_secret) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
"Browser OAuth clients must be public clients; staticClientInfo.client_secret is not allowed."
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
this.serverUrl = serverUrl;
|
|
1193
|
+
this.storage = new LocalStorageKVStore();
|
|
1194
|
+
this.session = new OAuthSessionStore(
|
|
1195
|
+
serverUrl,
|
|
1196
|
+
{ ...options, allowClientSecret: false },
|
|
1197
|
+
this.storage
|
|
1198
|
+
);
|
|
1199
|
+
this.preventAutoAuth = options.preventAutoAuth;
|
|
1200
|
+
this.useRedirectFlow = options.useRedirectFlow;
|
|
1201
|
+
this.oauthProxyUrl = options.oauthProxyUrl;
|
|
1202
|
+
this.connectionUrl = options.connectionUrl;
|
|
1203
|
+
this.proxyOAuthRequests = options.proxyOAuthRequests ?? true;
|
|
1204
|
+
this.staticClientInfo = options.staticClientInfo;
|
|
1205
|
+
this.onPopupWindow = options.onPopupWindow;
|
|
1206
|
+
}
|
|
1207
|
+
// --- Identity / key fields exposed for callback handling ---
|
|
1208
|
+
/** Prefix used for persisted OAuth keys. */
|
|
1209
|
+
get storageKeyPrefix() {
|
|
1210
|
+
return this.session.storageKeyPrefix;
|
|
1211
|
+
}
|
|
1212
|
+
/** Stable hash used to namespace storage for this server. */
|
|
1213
|
+
get serverUrlHash() {
|
|
1214
|
+
return this.session.serverUrlHash;
|
|
1215
|
+
}
|
|
1216
|
+
/** Human-readable OAuth client name. */
|
|
1217
|
+
get clientName() {
|
|
1218
|
+
return this.session.clientName;
|
|
1219
|
+
}
|
|
1220
|
+
/** Public website describing the OAuth client. */
|
|
1221
|
+
get clientUri() {
|
|
1222
|
+
return this.session.clientUri;
|
|
1223
|
+
}
|
|
1224
|
+
/** Public OAuth client logo URL. */
|
|
1225
|
+
get logoUri() {
|
|
1226
|
+
return this.session.logoUri;
|
|
1227
|
+
}
|
|
1228
|
+
/** OAuth redirect URI. */
|
|
1229
|
+
get callbackUrl() {
|
|
1230
|
+
return this.session.callbackUrl;
|
|
1231
|
+
}
|
|
1232
|
+
/** Space-delimited OAuth scopes requested by the client. */
|
|
1233
|
+
get scope() {
|
|
1234
|
+
return this.session.scope;
|
|
1235
|
+
}
|
|
1236
|
+
get clientMetadataUrl() {
|
|
1237
|
+
return this.session.clientMetadataUrl;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Returns a provider-scoped storage key.
|
|
1241
|
+
*
|
|
1242
|
+
* @param keySuffix - Suffix identifying the stored value.
|
|
1243
|
+
* @returns Namespaced storage key.
|
|
1244
|
+
*/
|
|
1245
|
+
getKey(keySuffix) {
|
|
1246
|
+
return this.session.getKey(keySuffix);
|
|
1247
|
+
}
|
|
1248
|
+
/** Whether an authorization flow is awaiting completion. */
|
|
1249
|
+
get hasPendingFlow() {
|
|
1250
|
+
return this.authorizationPending;
|
|
1251
|
+
}
|
|
1252
|
+
/** Marks the current authorization flow as complete. */
|
|
1253
|
+
markFlowComplete() {
|
|
1254
|
+
this.authorizationPending = false;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Re-anchor an SDK-derived OAuth discovery URL from the MCP connection
|
|
1258
|
+
* (proxy) origin onto the actual MCP server.
|
|
1259
|
+
*
|
|
1260
|
+
* When MCP traffic is tunneled through a gateway/inspector proxy, the SDK
|
|
1261
|
+
* transport derives `/.well-known/*` URLs from the URL it connected to (the
|
|
1262
|
+
* proxy) whenever no `resource_metadata` hint is available — the SSE
|
|
1263
|
+
* transport's EventSource cannot read `WWW-Authenticate`, and token refresh
|
|
1264
|
+
* runs without a 401 response at hand. The proxy origin serves no OAuth
|
|
1265
|
+
* metadata, so discovery would fail and the server would be misclassified
|
|
1266
|
+
* as "does not support OAuth". Rewriting reproduces what a direct
|
|
1267
|
+
* connection would have requested: the same well-known document, anchored
|
|
1268
|
+
* on the server origin, with the RFC 8414 §3.1 / RFC 9728 §3.1 path
|
|
1269
|
+
* insertion using the server's path instead of the proxy's.
|
|
1270
|
+
*/
|
|
1271
|
+
reanchorWellKnownUrl(url) {
|
|
1272
|
+
if (!this.connectionUrl) return url;
|
|
1273
|
+
try {
|
|
1274
|
+
const requested = new URL(url);
|
|
1275
|
+
const connection = new URL(this.connectionUrl);
|
|
1276
|
+
if (requested.origin !== connection.origin) return url;
|
|
1277
|
+
if (!requested.pathname.startsWith("/.well-known/")) return url;
|
|
1278
|
+
const target = new URL(this.serverUrl);
|
|
1279
|
+
const rest = requested.pathname.slice("/.well-known/".length);
|
|
1280
|
+
const [doc, ...suffixParts] = rest.split("/");
|
|
1281
|
+
if (!doc) return url;
|
|
1282
|
+
const suffix = suffixParts.length ? `/${suffixParts.join("/")}` : "";
|
|
1283
|
+
const connectionPath = trimTrailingSlashes(connection.pathname);
|
|
1284
|
+
const targetPath = trimTrailingSlashes(target.pathname);
|
|
1285
|
+
const newSuffix = suffix && suffix === connectionPath ? targetPath : suffix;
|
|
1286
|
+
return `${target.origin}/.well-known/${doc}${newSuffix}${requested.search}`;
|
|
1287
|
+
} catch {
|
|
1288
|
+
return url;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
rememberResourceMetadataChallenge(response) {
|
|
1292
|
+
if (response.status !== 401) return false;
|
|
1293
|
+
const { resourceMetadataUrl } = extractWWWAuthenticateParams(response);
|
|
1294
|
+
if (!resourceMetadataUrl) return false;
|
|
1295
|
+
this.challengedResourceMetadataUrl = resourceMetadataUrl.toString();
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Returns a `fetch` function, scoped to this provider, that routes OAuth
|
|
1300
|
+
* metadata and non-browser OAuth endpoint requests through the configured
|
|
1301
|
+
* `oauthProxyUrl` to bypass CORS. Authorization endpoints are navigated by
|
|
1302
|
+
* the browser and all unrelated requests pass through unchanged.
|
|
1303
|
+
*
|
|
1304
|
+
* Unlike patching the global `fetch`, the returned function only affects the
|
|
1305
|
+
* transport/auth calls it is explicitly handed to (via the SDK transport's
|
|
1306
|
+
* `fetch` option or `auth({ fetchFn })`). Connecting one server "Via Proxy"
|
|
1307
|
+
* therefore never alters fetch behavior for other servers, other
|
|
1308
|
+
* connections, or the rest of the page.
|
|
1309
|
+
*
|
|
1310
|
+
* OAuth metadata is always fetched with `cache: "no-store"`, including in
|
|
1311
|
+
* direct mode. Authorization servers commonly vary CORS headers by Origin;
|
|
1312
|
+
* bypassing the browser HTTP cache prevents a revalidated response cached
|
|
1313
|
+
* for another localhost origin from poisoning discovery. When OAuth proxying
|
|
1314
|
+
* is disabled or no `oauthProxyUrl` is configured, all requests still go
|
|
1315
|
+
* directly to their original URLs.
|
|
1316
|
+
*
|
|
1317
|
+
* @param baseFetch - The fetch used for non-OAuth requests and for the
|
|
1318
|
+
* underlying proxy calls. Defaults to the global `fetch`.
|
|
1319
|
+
*/
|
|
1320
|
+
getProxyFetch(baseFetch) {
|
|
1321
|
+
const base = baseFetch ?? globalThis.fetch.bind(globalThis);
|
|
1322
|
+
const oauthProxyUrl = this.proxyOAuthRequests && this.oauthProxyUrl ? this.oauthProxyUrl : void 0;
|
|
1323
|
+
const discoveredEndpoints = /* @__PURE__ */ new Set();
|
|
1324
|
+
let restoredDiscovery = false;
|
|
1325
|
+
return async (input, init) => {
|
|
1326
|
+
const requestedUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
1327
|
+
const url = this.reanchorWellKnownUrl(requestedUrl);
|
|
1328
|
+
let pathname;
|
|
1329
|
+
try {
|
|
1330
|
+
pathname = new URL(url).pathname;
|
|
1331
|
+
} catch {
|
|
1332
|
+
return await base(input, init);
|
|
1333
|
+
}
|
|
1334
|
+
const isMetadata = pathname.includes("/.well-known/");
|
|
1335
|
+
if (!oauthProxyUrl) {
|
|
1336
|
+
const response2 = await base(
|
|
1337
|
+
isMetadata ? url : input,
|
|
1338
|
+
isMetadata ? { ...init, cache: "no-store" } : init
|
|
1339
|
+
);
|
|
1340
|
+
if (!isMetadata) this.rememberResourceMetadataChallenge(response2);
|
|
1341
|
+
return response2;
|
|
1342
|
+
}
|
|
1343
|
+
if (!restoredDiscovery) {
|
|
1344
|
+
restoredDiscovery = true;
|
|
1345
|
+
const metadata = (await this.discoveryState())?.authorizationServerMetadata;
|
|
1346
|
+
for (const key of [
|
|
1347
|
+
"registration_endpoint",
|
|
1348
|
+
"token_endpoint",
|
|
1349
|
+
"revocation_endpoint",
|
|
1350
|
+
"introspection_endpoint"
|
|
1351
|
+
]) {
|
|
1352
|
+
if (typeof metadata?.[key] === "string") {
|
|
1353
|
+
discoveredEndpoints.add(metadata[key]);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const isProxiedEndpoint = discoveredEndpoints.has(url) || /\/(?:register|registration|token|revoke|revocation|introspect|introspection)\/?$/.test(
|
|
1358
|
+
pathname
|
|
1359
|
+
);
|
|
1360
|
+
if (!isMetadata && !isProxiedEndpoint) {
|
|
1361
|
+
const response2 = await base(input, init);
|
|
1362
|
+
if (this.rememberResourceMetadataChallenge(response2)) {
|
|
1363
|
+
discoveredEndpoints.clear();
|
|
1364
|
+
}
|
|
1365
|
+
return response2;
|
|
1366
|
+
}
|
|
1367
|
+
try {
|
|
1368
|
+
const urlObj = new URL(url);
|
|
1369
|
+
const proxyUrlObj = new URL(oauthProxyUrl);
|
|
1370
|
+
if (urlObj.origin === proxyUrlObj.origin && (urlObj.pathname.startsWith(proxyUrlObj.pathname) || url.includes("/inspector/api/oauth"))) {
|
|
1371
|
+
return await base(input, init);
|
|
1372
|
+
}
|
|
1373
|
+
} catch {
|
|
1374
|
+
}
|
|
1375
|
+
const proxyEndpoint = isMetadata ? `${oauthProxyUrl}/metadata?serverUrl=${encodeURIComponent(
|
|
1376
|
+
this.serverUrl
|
|
1377
|
+
)}&url=${encodeURIComponent(url)}` : `${oauthProxyUrl}/proxy`;
|
|
1378
|
+
if (isMetadata) {
|
|
1379
|
+
const response2 = await base(proxyEndpoint, {
|
|
1380
|
+
...init,
|
|
1381
|
+
method: "GET",
|
|
1382
|
+
cache: "no-store"
|
|
1383
|
+
});
|
|
1384
|
+
try {
|
|
1385
|
+
const metadata = await response2.clone().json();
|
|
1386
|
+
for (const key of [
|
|
1387
|
+
"registration_endpoint",
|
|
1388
|
+
"token_endpoint",
|
|
1389
|
+
"revocation_endpoint",
|
|
1390
|
+
"introspection_endpoint"
|
|
1391
|
+
]) {
|
|
1392
|
+
if (typeof metadata[key] === "string") {
|
|
1393
|
+
discoveredEndpoints.add(metadata[key]);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
return response2;
|
|
1399
|
+
}
|
|
1400
|
+
const inputRequest = input instanceof Request ? input : void 0;
|
|
1401
|
+
const method = init?.method ?? inputRequest?.method ?? "POST";
|
|
1402
|
+
const requestHeaders = init?.headers ?? inputRequest?.headers;
|
|
1403
|
+
let body;
|
|
1404
|
+
if (init?.body !== void 0 && init.body !== null) {
|
|
1405
|
+
body = await serializeBody(init.body);
|
|
1406
|
+
} else if (inputRequest?.body && method !== "GET" && method !== "HEAD") {
|
|
1407
|
+
body = await inputRequest.clone().text();
|
|
1408
|
+
}
|
|
1409
|
+
const response = await base(proxyEndpoint, {
|
|
1410
|
+
method: "POST",
|
|
1411
|
+
headers: { "Content-Type": "application/json" },
|
|
1412
|
+
body: JSON.stringify({
|
|
1413
|
+
serverUrl: this.serverUrl,
|
|
1414
|
+
url,
|
|
1415
|
+
method,
|
|
1416
|
+
headers: requestHeaders ? Object.fromEntries(new Headers(requestHeaders)) : {},
|
|
1417
|
+
body
|
|
1418
|
+
})
|
|
1419
|
+
});
|
|
1420
|
+
const data = await response.json();
|
|
1421
|
+
if (!response.ok || typeof data.status !== "number") {
|
|
1422
|
+
return new Response(JSON.stringify(data), {
|
|
1423
|
+
status: response.status,
|
|
1424
|
+
statusText: response.statusText,
|
|
1425
|
+
headers: response.headers
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
return new Response(JSON.stringify(data.body), {
|
|
1429
|
+
status: data.status,
|
|
1430
|
+
statusText: typeof data.statusText === "string" ? data.statusText : void 0,
|
|
1431
|
+
headers: new Headers(
|
|
1432
|
+
data.headers && typeof data.headers === "object" ? data.headers : void 0
|
|
1433
|
+
)
|
|
1434
|
+
});
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
// --- SDK Interface Methods (delegated) ---
|
|
1438
|
+
get redirectUrl() {
|
|
1439
|
+
return this.session.redirectUrl;
|
|
1440
|
+
}
|
|
1441
|
+
get clientMetadata() {
|
|
1442
|
+
return this.session.clientMetadata;
|
|
1443
|
+
}
|
|
1444
|
+
tokens(ctx) {
|
|
1445
|
+
return this.session.tokens(ctx);
|
|
1446
|
+
}
|
|
1447
|
+
saveTokens(tokens, ctx) {
|
|
1448
|
+
this.lastAttemptedAuthUrl = null;
|
|
1449
|
+
this.authorizationPending = false;
|
|
1450
|
+
return this.session.saveTokens(tokens, ctx);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Returns the configured or dynamically registered OAuth client information.
|
|
1454
|
+
*
|
|
1455
|
+
* @param ctx - Optional registration context.
|
|
1456
|
+
* @returns OAuth client information, or `undefined` when not registered.
|
|
1457
|
+
*/
|
|
1458
|
+
async clientInformation(ctx) {
|
|
1459
|
+
if (this.staticClientInfo) return this.staticClientInfo;
|
|
1460
|
+
return this.session.clientInformation(ctx);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Persists public OAuth client registration information.
|
|
1464
|
+
*
|
|
1465
|
+
* Static client configuration takes precedence, and browser providers discard
|
|
1466
|
+
* any client secret returned for a public client.
|
|
1467
|
+
*
|
|
1468
|
+
* @param clientInformation - Registration information to save.
|
|
1469
|
+
* @param ctx - Optional registration context.
|
|
1470
|
+
*/
|
|
1471
|
+
async saveClientInformation(clientInformation, ctx) {
|
|
1472
|
+
if (this.staticClientInfo) return;
|
|
1473
|
+
const { client_secret: discardedClientSecret, ...publicClientInformation } = clientInformation;
|
|
1474
|
+
if (discardedClientSecret) {
|
|
1475
|
+
console.info(
|
|
1476
|
+
`[${this.storageKeyPrefix}] Discarded client_secret returned for a public browser OAuth client.`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
return this.session.saveClientInformation(
|
|
1480
|
+
publicClientInformation,
|
|
1481
|
+
ctx
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
codeVerifier() {
|
|
1485
|
+
return this.session.codeVerifier();
|
|
1486
|
+
}
|
|
1487
|
+
saveCodeVerifier(codeVerifier) {
|
|
1488
|
+
return this.session.saveCodeVerifier(codeVerifier);
|
|
1489
|
+
}
|
|
1490
|
+
invalidateCredentials(scope) {
|
|
1491
|
+
return this.session.invalidateCredentials(scope);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Persist OAuth discovery state (SEP-2352). Delegated to the session store;
|
|
1495
|
+
* implementing this silences the SDK's per-callback warning and enables the
|
|
1496
|
+
* authorization-server mix-up defense on the callback leg.
|
|
1497
|
+
*/
|
|
1498
|
+
saveDiscoveryState(state) {
|
|
1499
|
+
return this.session.saveDiscoveryState(state);
|
|
1500
|
+
}
|
|
1501
|
+
/** Return previously saved OAuth discovery state, or `undefined`. */
|
|
1502
|
+
async discoveryState() {
|
|
1503
|
+
const state = await this.session.discoveryState();
|
|
1504
|
+
const challengedUrl = this.challengedResourceMetadataUrl;
|
|
1505
|
+
this.challengedResourceMetadataUrl = void 0;
|
|
1506
|
+
if (challengedUrl && state) {
|
|
1507
|
+
await this.session.invalidateCredentials("discovery");
|
|
1508
|
+
return void 0;
|
|
1509
|
+
}
|
|
1510
|
+
return state;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Return the token endpoint from the SDK's persisted discovery state.
|
|
1514
|
+
* Returns `null` before a successful authorization discovery.
|
|
1515
|
+
*/
|
|
1516
|
+
getTokenEndpoint() {
|
|
1517
|
+
return this.session.getTokenEndpoint();
|
|
1518
|
+
}
|
|
1519
|
+
/** Return the protected-resource URL selected during OAuth discovery. */
|
|
1520
|
+
getResource() {
|
|
1521
|
+
return this.session.getResource();
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Return the stored public OAuth client ID. Browser providers do not retain
|
|
1525
|
+
* client secrets.
|
|
1526
|
+
*/
|
|
1527
|
+
async getClientCredentials() {
|
|
1528
|
+
const info = await this.clientInformation();
|
|
1529
|
+
return info?.client_id ? { client_id: info.client_id } : null;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Generates and persists `StoredState` for an authorization request,
|
|
1533
|
+
* and returns the sanitized URL with the `state` param appended. Does NOT
|
|
1534
|
+
* open a popup or redirect —
|
|
1535
|
+
* use `redirectToAuthorization` for that.
|
|
1536
|
+
*/
|
|
1537
|
+
async prepareAuthorizationUrl(authorizationUrl) {
|
|
1538
|
+
const prepared = await this.session.storeAuthorizationState(
|
|
1539
|
+
authorizationUrl,
|
|
1540
|
+
{
|
|
1541
|
+
extraProviderOptions: {
|
|
1542
|
+
oauthProxyUrl: this.oauthProxyUrl,
|
|
1543
|
+
...this.clientMetadataUrl ? { clientMetadataUrl: this.clientMetadataUrl } : {},
|
|
1544
|
+
...this.staticClientInfo ? { staticClientInfo: this.staticClientInfo } : {},
|
|
1545
|
+
...this.scope ? { scope: this.scope } : {}
|
|
1546
|
+
},
|
|
1547
|
+
flowType: this.useRedirectFlow ? "redirect" : "popup",
|
|
1548
|
+
returnUrl: typeof window !== "undefined" ? window.location.href : void 0
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1551
|
+
this.lastAttemptedAuthUrl = prepared;
|
|
1552
|
+
this.authorizationPending = true;
|
|
1553
|
+
return prepared;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Redirects the user agent to the authorization URL, storing necessary state.
|
|
1557
|
+
* @param authorizationUrl - The fully constructed authorization URL from the SDK.
|
|
1558
|
+
*/
|
|
1559
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
1560
|
+
await this.prepareAuthorizationUrl(authorizationUrl);
|
|
1561
|
+
if (this.preventAutoAuth) {
|
|
1562
|
+
console.info(
|
|
1563
|
+
`[${this.storageKeyPrefix}] Auto-auth prevented. Authorization URL stored for manual trigger.`
|
|
1564
|
+
);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
this.startAuthorization();
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Open the authorization URL prepared by the official SDK.
|
|
1571
|
+
*
|
|
1572
|
+
* This is the explicit-user-action counterpart to `preventAutoAuth`: the
|
|
1573
|
+
* provider still lets the SDK own discovery and PKCE state, while a host can
|
|
1574
|
+
* launch the stored authorization request later from an Authenticate button.
|
|
1575
|
+
*/
|
|
1576
|
+
startAuthorization() {
|
|
1577
|
+
const authorizationUrl = this.lastAttemptedAuthUrl;
|
|
1578
|
+
if (!authorizationUrl) {
|
|
1579
|
+
throw new Error("No prepared OAuth authorization is available");
|
|
1580
|
+
}
|
|
1581
|
+
if (this.useRedirectFlow) {
|
|
1582
|
+
console.info(
|
|
1583
|
+
`[${this.storageKeyPrefix}] Redirecting to authorization URL (full-page redirect).`
|
|
1584
|
+
);
|
|
1585
|
+
window.location.href = authorizationUrl;
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
const popupFeatures = "width=600,height=700,resizable=yes,scrollbars=yes,status=yes";
|
|
1589
|
+
try {
|
|
1590
|
+
const popup = window.open(
|
|
1591
|
+
authorizationUrl,
|
|
1592
|
+
`mcp_auth_${this.serverUrlHash}`,
|
|
1593
|
+
popupFeatures
|
|
1594
|
+
);
|
|
1595
|
+
if (this.onPopupWindow) {
|
|
1596
|
+
this.onPopupWindow(authorizationUrl, popupFeatures, popup);
|
|
1597
|
+
}
|
|
1598
|
+
if (!popup || popup.closed || typeof popup.closed === "undefined") {
|
|
1599
|
+
console.warn(
|
|
1600
|
+
`[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`
|
|
1601
|
+
);
|
|
1602
|
+
} else {
|
|
1603
|
+
popup.focus();
|
|
1604
|
+
console.info(
|
|
1605
|
+
`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
} catch (e) {
|
|
1609
|
+
console.error(
|
|
1610
|
+
`[${this.storageKeyPrefix}] Error opening popup window:`,
|
|
1611
|
+
e
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback.
|
|
1617
|
+
*/
|
|
1618
|
+
getLastAttemptedAuthUrl() {
|
|
1619
|
+
return this.lastAttemptedAuthUrl;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Removes OAuth state stored for this server.
|
|
1623
|
+
*
|
|
1624
|
+
* @returns The number of storage entries removed.
|
|
1625
|
+
*/
|
|
1626
|
+
clearStorage() {
|
|
1627
|
+
this.lastAttemptedAuthUrl = null;
|
|
1628
|
+
this.authorizationPending = false;
|
|
1629
|
+
const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`;
|
|
1630
|
+
const keysToRemove = [];
|
|
1631
|
+
let count = 0;
|
|
1632
|
+
for (const key of this.storage.keys()) {
|
|
1633
|
+
if (key.startsWith(prefixPattern)) {
|
|
1634
|
+
keysToRemove.push(key);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const uniqueKeysToRemove = [...new Set(keysToRemove)];
|
|
1638
|
+
uniqueKeysToRemove.forEach((key) => {
|
|
1639
|
+
this.storage.remove(key);
|
|
1640
|
+
count++;
|
|
1641
|
+
});
|
|
1642
|
+
return count;
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
async function createOAuthProvider(serverUrl, options = {}) {
|
|
1646
|
+
return new BrowserOAuthClientProvider(serverUrl, options);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/auth/callback.ts
|
|
1650
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
1651
|
+
|
|
1652
|
+
// src/auth/popup.ts
|
|
1653
|
+
var MCP_AUTH_BROADCAST_CHANNEL = "mcp_auth_callback";
|
|
1654
|
+
var MCP_AUTH_CALLBACK_MESSAGE_TYPE = "mcp_auth_callback";
|
|
1655
|
+
function hasStoredTokens(tokensKey) {
|
|
1656
|
+
try {
|
|
1657
|
+
return typeof localStorage !== "undefined" && !!localStorage.getItem(tokensKey);
|
|
1658
|
+
} catch {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function runAuthPopup({
|
|
1663
|
+
popup,
|
|
1664
|
+
state,
|
|
1665
|
+
tokensKey,
|
|
1666
|
+
timeoutMs = 5 * 6e4,
|
|
1667
|
+
closePollMs = 1e3,
|
|
1668
|
+
closeGraceMs = 2e4,
|
|
1669
|
+
expectedOrigin = typeof window !== "undefined" ? window.location.origin : ""
|
|
1670
|
+
}) {
|
|
1671
|
+
return new Promise((resolve) => {
|
|
1672
|
+
let settled = false;
|
|
1673
|
+
let closeTimer = null;
|
|
1674
|
+
let timeoutTimer = null;
|
|
1675
|
+
let graceTimer = null;
|
|
1676
|
+
let broadcastChannel = null;
|
|
1677
|
+
const cleanup = () => {
|
|
1678
|
+
if (closeTimer) {
|
|
1679
|
+
clearInterval(closeTimer);
|
|
1680
|
+
closeTimer = null;
|
|
1681
|
+
}
|
|
1682
|
+
if (timeoutTimer) {
|
|
1683
|
+
clearTimeout(timeoutTimer);
|
|
1684
|
+
timeoutTimer = null;
|
|
1685
|
+
}
|
|
1686
|
+
if (graceTimer) {
|
|
1687
|
+
clearTimeout(graceTimer);
|
|
1688
|
+
graceTimer = null;
|
|
1689
|
+
}
|
|
1690
|
+
if (typeof window !== "undefined") {
|
|
1691
|
+
window.removeEventListener("message", messageHandler);
|
|
1692
|
+
window.removeEventListener("storage", storageHandler);
|
|
1693
|
+
}
|
|
1694
|
+
if (broadcastChannel) {
|
|
1695
|
+
try {
|
|
1696
|
+
broadcastChannel.removeEventListener("message", broadcastHandler);
|
|
1697
|
+
broadcastChannel.close();
|
|
1698
|
+
} catch {
|
|
1699
|
+
}
|
|
1700
|
+
broadcastChannel = null;
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
const settle = (result) => {
|
|
1704
|
+
if (settled) return;
|
|
1705
|
+
settled = true;
|
|
1706
|
+
cleanup();
|
|
1707
|
+
resolve(result);
|
|
1708
|
+
};
|
|
1709
|
+
const handlePayload = (payload) => {
|
|
1710
|
+
if (!payload || payload.type !== MCP_AUTH_CALLBACK_MESSAGE_TYPE) return;
|
|
1711
|
+
if (payload.state && state && payload.state !== state) return;
|
|
1712
|
+
if (payload.success) {
|
|
1713
|
+
settle({ kind: "success" });
|
|
1714
|
+
} else {
|
|
1715
|
+
settle({
|
|
1716
|
+
kind: "error",
|
|
1717
|
+
error: payload.error ?? "Authentication failed in callback."
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
const messageHandler = (event) => {
|
|
1722
|
+
if (expectedOrigin && event.origin !== expectedOrigin) return;
|
|
1723
|
+
handlePayload(event.data);
|
|
1724
|
+
};
|
|
1725
|
+
const broadcastHandler = (event) => {
|
|
1726
|
+
handlePayload(event.data);
|
|
1727
|
+
};
|
|
1728
|
+
const storageHandler = (event) => {
|
|
1729
|
+
if (event.key !== tokensKey) return;
|
|
1730
|
+
if (event.newValue) settle({ kind: "success" });
|
|
1731
|
+
};
|
|
1732
|
+
if (typeof window !== "undefined") {
|
|
1733
|
+
window.addEventListener("message", messageHandler);
|
|
1734
|
+
window.addEventListener("storage", storageHandler);
|
|
1735
|
+
}
|
|
1736
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
1737
|
+
try {
|
|
1738
|
+
broadcastChannel = new BroadcastChannel(MCP_AUTH_BROADCAST_CHANNEL);
|
|
1739
|
+
broadcastChannel.addEventListener("message", broadcastHandler);
|
|
1740
|
+
} catch {
|
|
1741
|
+
broadcastChannel = null;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (popup) {
|
|
1745
|
+
closeTimer = setInterval(() => {
|
|
1746
|
+
if (settled) return;
|
|
1747
|
+
let closed = false;
|
|
1748
|
+
try {
|
|
1749
|
+
closed = popup.closed;
|
|
1750
|
+
} catch {
|
|
1751
|
+
closed = false;
|
|
1752
|
+
}
|
|
1753
|
+
if (!closed) return;
|
|
1754
|
+
if (closeTimer) {
|
|
1755
|
+
clearInterval(closeTimer);
|
|
1756
|
+
closeTimer = null;
|
|
1757
|
+
}
|
|
1758
|
+
if (hasStoredTokens(tokensKey)) {
|
|
1759
|
+
settle({ kind: "success" });
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
graceTimer = setTimeout(() => {
|
|
1763
|
+
settle(
|
|
1764
|
+
hasStoredTokens(tokensKey) ? { kind: "success" } : { kind: "cancelled" }
|
|
1765
|
+
);
|
|
1766
|
+
}, closeGraceMs);
|
|
1767
|
+
}, closePollMs);
|
|
1768
|
+
}
|
|
1769
|
+
timeoutTimer = setTimeout(() => {
|
|
1770
|
+
settle(
|
|
1771
|
+
hasStoredTokens(tokensKey) ? { kind: "success" } : { kind: "timeout" }
|
|
1772
|
+
);
|
|
1773
|
+
}, timeoutMs);
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/auth/callback.ts
|
|
1778
|
+
var inFlightCallback = null;
|
|
1779
|
+
function isMcpAuthPopupWindow() {
|
|
1780
|
+
return typeof window !== "undefined" && window.name.startsWith("mcp_auth_");
|
|
1781
|
+
}
|
|
1782
|
+
function buildCallbackPayload(success, error, meta) {
|
|
1783
|
+
return {
|
|
1784
|
+
type: MCP_AUTH_CALLBACK_MESSAGE_TYPE,
|
|
1785
|
+
success,
|
|
1786
|
+
...success ? {} : { error: error ?? "Unknown error" },
|
|
1787
|
+
...meta.state ? { state: meta.state } : {},
|
|
1788
|
+
...meta.serverUrlHash ? { serverUrlHash: meta.serverUrlHash } : {}
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
function broadcastCallback(payload) {
|
|
1792
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
1793
|
+
let channel;
|
|
1794
|
+
try {
|
|
1795
|
+
channel = new BroadcastChannel(MCP_AUTH_BROADCAST_CHANNEL);
|
|
1796
|
+
channel.postMessage(payload);
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
console.warn("[mcp-callback] Failed to broadcast callback result:", error);
|
|
1799
|
+
} finally {
|
|
1800
|
+
if (channel) {
|
|
1801
|
+
setTimeout(() => {
|
|
1802
|
+
try {
|
|
1803
|
+
channel?.close();
|
|
1804
|
+
} catch {
|
|
1805
|
+
}
|
|
1806
|
+
}, 0);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function renderResult(title, message, error, returnUrl) {
|
|
1811
|
+
if (typeof document === "undefined") return;
|
|
1812
|
+
document.body.innerHTML = "";
|
|
1813
|
+
const container = document.createElement("div");
|
|
1814
|
+
container.style.fontFamily = "sans-serif";
|
|
1815
|
+
container.style.padding = "20px";
|
|
1816
|
+
const heading = document.createElement("h1");
|
|
1817
|
+
heading.textContent = title;
|
|
1818
|
+
container.appendChild(heading);
|
|
1819
|
+
const text = document.createElement("p");
|
|
1820
|
+
text.textContent = message;
|
|
1821
|
+
if (error) {
|
|
1822
|
+
text.style.color = "red";
|
|
1823
|
+
text.style.backgroundColor = "#ffebeb";
|
|
1824
|
+
text.style.border = "1px solid red";
|
|
1825
|
+
text.style.padding = "10px";
|
|
1826
|
+
text.style.borderRadius = "4px";
|
|
1827
|
+
}
|
|
1828
|
+
container.appendChild(text);
|
|
1829
|
+
const close = document.createElement("a");
|
|
1830
|
+
close.href = "#";
|
|
1831
|
+
close.textContent = "Close this window";
|
|
1832
|
+
close.onclick = (event) => {
|
|
1833
|
+
event.preventDefault();
|
|
1834
|
+
window.close();
|
|
1835
|
+
return false;
|
|
1836
|
+
};
|
|
1837
|
+
container.appendChild(close);
|
|
1838
|
+
if (returnUrl) {
|
|
1839
|
+
const separator = document.createTextNode(" or ");
|
|
1840
|
+
const back = document.createElement("a");
|
|
1841
|
+
back.href = returnUrl;
|
|
1842
|
+
back.textContent = "return to the app";
|
|
1843
|
+
container.append(separator, back);
|
|
1844
|
+
}
|
|
1845
|
+
document.body.appendChild(container);
|
|
1846
|
+
}
|
|
1847
|
+
async function findStoredState(state) {
|
|
1848
|
+
const store = new LocalStorageKVStore();
|
|
1849
|
+
const legacySuffix = `:state_${state}`;
|
|
1850
|
+
const scopedSuffix = `_state_${state}`;
|
|
1851
|
+
const key = (await store.keys()).find(
|
|
1852
|
+
(candidate) => candidate.endsWith(legacySuffix) || candidate.endsWith(scopedSuffix)
|
|
1853
|
+
);
|
|
1854
|
+
const serialized = key ? await store.get(key) : null;
|
|
1855
|
+
if (!key || !serialized) {
|
|
1856
|
+
throw new Error(`Invalid or expired OAuth state "${state}".`);
|
|
1857
|
+
}
|
|
1858
|
+
let value;
|
|
1859
|
+
try {
|
|
1860
|
+
value = JSON.parse(serialized);
|
|
1861
|
+
} catch {
|
|
1862
|
+
await store.remove(key);
|
|
1863
|
+
throw new Error("Failed to parse stored OAuth state.");
|
|
1864
|
+
}
|
|
1865
|
+
return { key, value, store };
|
|
1866
|
+
}
|
|
1867
|
+
function redirectWithError(returnUrl, message) {
|
|
1868
|
+
const url = new URL(returnUrl);
|
|
1869
|
+
url.searchParams.set("auth_error", "oauth_callback_failed");
|
|
1870
|
+
url.searchParams.set("auth_error_description", message);
|
|
1871
|
+
window.location.href = url.toString();
|
|
1872
|
+
}
|
|
1873
|
+
function signalResult(success, error, storedState, meta) {
|
|
1874
|
+
const payload = buildCallbackPayload(success, error, meta);
|
|
1875
|
+
const returnUrl = storedState?.returnUrl;
|
|
1876
|
+
const popup = storedState?.flowType === "popup" || isMcpAuthPopupWindow();
|
|
1877
|
+
if (storedState?.flowType === "redirect" && returnUrl) {
|
|
1878
|
+
if (success) window.location.href = returnUrl;
|
|
1879
|
+
else redirectWithError(returnUrl, error ?? "Authentication failed.");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (window.opener && !window.opener.closed) {
|
|
1883
|
+
window.opener.postMessage(payload, window.location.origin);
|
|
1884
|
+
window.close();
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (popup) {
|
|
1888
|
+
broadcastCallback(payload);
|
|
1889
|
+
renderResult(
|
|
1890
|
+
success ? "Authentication Successful!" : "Authentication Error",
|
|
1891
|
+
success ? "You're authenticated. You can close this window and return to the app." : error ?? "Authentication failed.",
|
|
1892
|
+
!success,
|
|
1893
|
+
returnUrl
|
|
1894
|
+
);
|
|
1895
|
+
try {
|
|
1896
|
+
window.close();
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (returnUrl) {
|
|
1902
|
+
if (success) window.location.href = returnUrl;
|
|
1903
|
+
else redirectWithError(returnUrl, error ?? "Authentication failed.");
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (!success) {
|
|
1907
|
+
renderResult(
|
|
1908
|
+
"Authentication Error",
|
|
1909
|
+
error ?? "Authentication failed.",
|
|
1910
|
+
true
|
|
1911
|
+
);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
window.location.href = "/";
|
|
1915
|
+
}
|
|
1916
|
+
function onMcpAuthorization() {
|
|
1917
|
+
if (!inFlightCallback) inFlightCallback = completeAuthorization();
|
|
1918
|
+
return inFlightCallback;
|
|
1919
|
+
}
|
|
1920
|
+
async function completeAuthorization() {
|
|
1921
|
+
const callbackParams = new URLSearchParams(window.location.search);
|
|
1922
|
+
const state = callbackParams.get("state");
|
|
1923
|
+
let stateKey = null;
|
|
1924
|
+
let stateStore = null;
|
|
1925
|
+
let storedState = null;
|
|
1926
|
+
let provider = null;
|
|
1927
|
+
try {
|
|
1928
|
+
if (!state) {
|
|
1929
|
+
throw new Error("OAuth callback is missing the state parameter.");
|
|
1930
|
+
}
|
|
1931
|
+
const stored = await findStoredState(state);
|
|
1932
|
+
stateKey = stored.key;
|
|
1933
|
+
stateStore = stored.store;
|
|
1934
|
+
storedState = stored.value;
|
|
1935
|
+
if (!storedState.expiry || storedState.expiry < Date.now()) {
|
|
1936
|
+
await stateStore.remove(stateKey);
|
|
1937
|
+
throw new Error(
|
|
1938
|
+
"OAuth state has expired. Please start authentication again."
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
if (!storedState.providerOptions) {
|
|
1942
|
+
throw new Error("Stored OAuth state is missing provider options.");
|
|
1943
|
+
}
|
|
1944
|
+
const { serverUrl, ...providerOptions } = storedState.providerOptions;
|
|
1945
|
+
provider = new BrowserOAuthClientProvider(serverUrl, providerOptions);
|
|
1946
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
1947
|
+
authProvider: provider,
|
|
1948
|
+
fetch: provider.getProxyFetch()
|
|
1949
|
+
});
|
|
1950
|
+
await transport.finishAuth(callbackParams);
|
|
1951
|
+
await stateStore.remove(stateKey);
|
|
1952
|
+
signalResult(true, void 0, storedState, {
|
|
1953
|
+
state,
|
|
1954
|
+
serverUrlHash: storedState.serverUrlHash
|
|
1955
|
+
});
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1958
|
+
console.error("[mcp-callback] OAuth callback failed:", error);
|
|
1959
|
+
if (stateKey && stateStore) await stateStore.remove(stateKey);
|
|
1960
|
+
if (provider) {
|
|
1961
|
+
await (stateStore ?? new LocalStorageKVStore()).remove(
|
|
1962
|
+
provider.getKey("last_auth_url")
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
signalResult(false, message, storedState, {
|
|
1966
|
+
state,
|
|
1967
|
+
serverUrlHash: storedState?.serverUrlHash
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/auth/flow.ts
|
|
1973
|
+
import {
|
|
1974
|
+
auth,
|
|
1975
|
+
InsufficientScopeError,
|
|
1976
|
+
UnauthorizedError
|
|
1977
|
+
} from "@modelcontextprotocol/client";
|
|
1978
|
+
var DEFAULT_AUTH_TIMEOUT_MS = 5 * 6e4;
|
|
1979
|
+
function isUnauthorized(err, depth = 0) {
|
|
1980
|
+
if (!err || depth > 5) return false;
|
|
1981
|
+
if (err instanceof UnauthorizedError) return true;
|
|
1982
|
+
if (err instanceof Error) {
|
|
1983
|
+
const code = err.code;
|
|
1984
|
+
if (code === 401) return true;
|
|
1985
|
+
if (err.name === "UnauthorizedError") return true;
|
|
1986
|
+
const message = err.message ?? "";
|
|
1987
|
+
if (message.includes("401") || message.includes("Unauthorized")) {
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
if (err.cause && isUnauthorized(err.cause, depth + 1)) return true;
|
|
1991
|
+
const data = err.data;
|
|
1992
|
+
if (data?.cause && isUnauthorized(data.cause, depth + 1)) return true;
|
|
1993
|
+
}
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
function isOAuthInteractionRequired(err, depth = 0) {
|
|
1997
|
+
if (!err || depth > 5) return false;
|
|
1998
|
+
if (err instanceof InsufficientScopeError || err instanceof UnauthorizedError) {
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
2001
|
+
if (err instanceof Error) {
|
|
2002
|
+
if (err.name === "InsufficientScopeError" || err.name === "UnauthorizedError") {
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
if (err.cause && isOAuthInteractionRequired(err.cause, depth + 1)) {
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
const data = err.data;
|
|
2009
|
+
if (data?.cause && isOAuthInteractionRequired(data.cause, depth + 1)) {
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
async function completeOAuthFlow(provider, serverUrl, options = {}) {
|
|
2016
|
+
const flowProvider = provider;
|
|
2017
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
|
|
2018
|
+
const fetchFn = options.fetchFn ?? flowProvider.getProxyFetch?.() ?? void 0;
|
|
2019
|
+
if (!flowProvider.hasPendingFlow) {
|
|
2020
|
+
const result = await auth(provider, { serverUrl, fetchFn });
|
|
2021
|
+
if (result === "AUTHORIZED") return;
|
|
2022
|
+
if (result !== "REDIRECT") {
|
|
2023
|
+
throw new Error(`Unexpected OAuth auth() result: ${result}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (flowProvider.preventAutoAuth === true && typeof flowProvider.startAuthorization === "function") {
|
|
2027
|
+
flowProvider.startAuthorization();
|
|
2028
|
+
}
|
|
2029
|
+
if (typeof flowProvider.getAuthorizationResponse === "function" || typeof flowProvider.getAuthorizationCode === "function") {
|
|
2030
|
+
const response = typeof flowProvider.getAuthorizationResponse === "function" ? await flowProvider.getAuthorizationResponse() : { code: await flowProvider.getAuthorizationCode() };
|
|
2031
|
+
if (options.finishAuthorization) {
|
|
2032
|
+
await options.finishAuthorization(response.code, response.iss);
|
|
2033
|
+
} else {
|
|
2034
|
+
await auth(provider, {
|
|
2035
|
+
serverUrl,
|
|
2036
|
+
authorizationCode: response.code,
|
|
2037
|
+
...response.iss !== void 0 ? { iss: response.iss } : {},
|
|
2038
|
+
fetchFn
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
await waitForBrowserAuthComplete(flowProvider, timeoutMs);
|
|
2044
|
+
}
|
|
2045
|
+
async function waitForBrowserAuthComplete(provider, timeoutMs) {
|
|
2046
|
+
if (typeof window === "undefined") {
|
|
2047
|
+
throw new Error(
|
|
2048
|
+
"OAuth redirect requires a browser environment or a provider with getAuthorizationCode()"
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
if (provider.useRedirectFlow) {
|
|
2052
|
+
await new Promise(() => {
|
|
2053
|
+
});
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const tokensKey = provider.getKey?.("tokens");
|
|
2057
|
+
if (!tokensKey) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
"Browser OAuth provider must expose getKey() for token storage"
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
let state = null;
|
|
2063
|
+
const authUrl = provider.getLastAttemptedAuthUrl?.();
|
|
2064
|
+
if (authUrl) {
|
|
2065
|
+
try {
|
|
2066
|
+
state = new URL(authUrl).searchParams.get("state");
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const result = await runAuthPopup({
|
|
2072
|
+
popup: null,
|
|
2073
|
+
state,
|
|
2074
|
+
tokensKey,
|
|
2075
|
+
timeoutMs
|
|
2076
|
+
});
|
|
2077
|
+
switch (result.kind) {
|
|
2078
|
+
case "success":
|
|
2079
|
+
return;
|
|
2080
|
+
case "cancelled":
|
|
2081
|
+
throw new Error("OAuth authentication was cancelled.");
|
|
2082
|
+
case "timeout":
|
|
2083
|
+
throw new Error(
|
|
2084
|
+
`OAuth callback not received within ${timeoutMs}ms. Ensure /oauth/callback calls onMcpAuthorization().`
|
|
2085
|
+
);
|
|
2086
|
+
case "error":
|
|
2087
|
+
throw new Error(result.error);
|
|
2088
|
+
default:
|
|
2089
|
+
throw new Error("Unexpected OAuth popup result");
|
|
2090
|
+
}
|
|
2091
|
+
} finally {
|
|
2092
|
+
provider.markFlowComplete?.();
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/index-browser.ts
|
|
2097
|
+
import { auth as auth2, UnauthorizedError as UnauthorizedError3 } from "@modelcontextprotocol/client";
|
|
2098
|
+
|
|
2099
|
+
// src/transport/http.ts
|
|
2100
|
+
import {
|
|
2101
|
+
Client,
|
|
2102
|
+
discoverOAuthProtectedResourceMetadata,
|
|
2103
|
+
SdkError,
|
|
2104
|
+
SdkHttpError,
|
|
2105
|
+
StreamableHTTPClientTransport as StreamableHTTPClientTransport2,
|
|
2106
|
+
UnauthorizedError as UnauthorizedError2
|
|
2107
|
+
} from "@modelcontextprotocol/client";
|
|
2108
|
+
|
|
2109
|
+
// src/utils/json-schema-validator.ts
|
|
2110
|
+
import {
|
|
2111
|
+
CfWorkerJsonSchemaValidator
|
|
2112
|
+
} from "@modelcontextprotocol/client/validators/cf-worker";
|
|
2113
|
+
var DRAFT_04_URI = "http://json-schema.org/draft-04/schema";
|
|
2114
|
+
var DRAFT_07_URIS = /* @__PURE__ */ new Set([
|
|
2115
|
+
"http://json-schema.org/draft-07/schema",
|
|
2116
|
+
"https://json-schema.org/draft-07/schema"
|
|
2117
|
+
]);
|
|
2118
|
+
var DRAFT_2019_09_URIS = /* @__PURE__ */ new Set([
|
|
2119
|
+
"https://json-schema.org/draft/2019-09/schema",
|
|
2120
|
+
"http://json-schema.org/draft/2019-09/schema"
|
|
2121
|
+
]);
|
|
2122
|
+
var DRAFT_2020_12_URIS = /* @__PURE__ */ new Set([
|
|
2123
|
+
"https://json-schema.org/draft/2020-12/schema",
|
|
2124
|
+
"http://json-schema.org/draft/2020-12/schema"
|
|
2125
|
+
]);
|
|
2126
|
+
function resolveDraft(schema) {
|
|
2127
|
+
if (!("$schema" in schema) || typeof schema.$schema !== "string") {
|
|
2128
|
+
return "2020-12";
|
|
2129
|
+
}
|
|
2130
|
+
const normalized = schema.$schema.replace(/#$/, "");
|
|
2131
|
+
if (normalized === DRAFT_04_URI) return "4";
|
|
2132
|
+
if (DRAFT_07_URIS.has(normalized)) return "7";
|
|
2133
|
+
if (DRAFT_2019_09_URIS.has(normalized)) return "2019-09";
|
|
2134
|
+
if (DRAFT_2020_12_URIS.has(normalized)) return "2020-12";
|
|
2135
|
+
return void 0;
|
|
2136
|
+
}
|
|
2137
|
+
var DialectJsonSchemaValidator = class {
|
|
2138
|
+
getValidator(schema) {
|
|
2139
|
+
const draft = resolveDraft(schema);
|
|
2140
|
+
const delegate = draft !== void 0 ? new CfWorkerJsonSchemaValidator({ draft }) : new CfWorkerJsonSchemaValidator();
|
|
2141
|
+
return delegate.getValidator(schema);
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
|
|
2145
|
+
// src/transport/base.ts
|
|
2146
|
+
var passthroughResultSchema = {
|
|
2147
|
+
"~standard": {
|
|
2148
|
+
version: 1,
|
|
2149
|
+
vendor: "mcp-use",
|
|
2150
|
+
validate: (value) => ({ value })
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
var BaseConnector = class {
|
|
2154
|
+
client = null;
|
|
2155
|
+
connectionManager = null;
|
|
2156
|
+
toolsCache = null;
|
|
2157
|
+
capabilitiesCache = null;
|
|
2158
|
+
serverInfoCache = null;
|
|
2159
|
+
authorizationCache;
|
|
2160
|
+
connected = false;
|
|
2161
|
+
opts;
|
|
2162
|
+
notificationHandlers = [];
|
|
2163
|
+
rootsCache = [];
|
|
2164
|
+
activeProgressHandlers = /* @__PURE__ */ new Set();
|
|
2165
|
+
/**
|
|
2166
|
+
* Creates a connector with shared SDK and callback options.
|
|
2167
|
+
*
|
|
2168
|
+
* @param opts - Connector initialization options.
|
|
2169
|
+
*/
|
|
2170
|
+
constructor(opts = {}) {
|
|
2171
|
+
this.opts = opts;
|
|
2172
|
+
if (opts.roots) {
|
|
2173
|
+
this.rootsCache = [...opts.roots];
|
|
2174
|
+
}
|
|
2175
|
+
if (opts.onNotification) {
|
|
2176
|
+
this.notificationHandlers.push(opts.onNotification);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Track connector initialization event
|
|
2181
|
+
* Should be called by subclasses after successful connection
|
|
2182
|
+
*/
|
|
2183
|
+
trackConnectorInit(data) {
|
|
2184
|
+
const connectorType = this.constructor.name;
|
|
2185
|
+
trackConnectorTelemetry({ connectorType, ...data });
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Register a handler for server notifications
|
|
2189
|
+
*
|
|
2190
|
+
* @param handler - Function to call when a notification is received
|
|
2191
|
+
*
|
|
2192
|
+
* @example
|
|
2193
|
+
* ```typescript
|
|
2194
|
+
* connector.onNotification((notification) => {
|
|
2195
|
+
* console.log(`Received: ${notification.method}`, notification.params);
|
|
2196
|
+
* });
|
|
2197
|
+
* ```
|
|
2198
|
+
*/
|
|
2199
|
+
onNotification(handler) {
|
|
2200
|
+
this.notificationHandlers.push(handler);
|
|
2201
|
+
if (this.client) {
|
|
2202
|
+
this.setupNotificationHandler();
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
/** Forward a normalized notification to every registered consumer. */
|
|
2206
|
+
async forwardNotification(notification) {
|
|
2207
|
+
for (const handler of this.notificationHandlers) {
|
|
2208
|
+
try {
|
|
2209
|
+
await handler(notification);
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
logger.error("Error in notification handler:", err);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
/** Handle SDK list-change callbacks identically on v1 and v2 connections. */
|
|
2216
|
+
async handleListChanged(method, error, tools) {
|
|
2217
|
+
if (error) {
|
|
2218
|
+
logger.warn(`[Auto] ${method} refresh failed:`, error);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (method === "notifications/tools/list_changed" && tools) {
|
|
2222
|
+
this.toolsCache = [...tools];
|
|
2223
|
+
}
|
|
2224
|
+
await this.forwardNotification({ method });
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Internal: wire notification handlers to the SDK client
|
|
2228
|
+
* Includes automatic handling for list_changed notifications per MCP spec
|
|
2229
|
+
*/
|
|
2230
|
+
setupNotificationHandler() {
|
|
2231
|
+
if (!this.client) return;
|
|
2232
|
+
this.client.fallbackNotificationHandler = async (notification) => {
|
|
2233
|
+
switch (notification.method) {
|
|
2234
|
+
case "notifications/tools/list_changed":
|
|
2235
|
+
await this.refreshToolsCache();
|
|
2236
|
+
break;
|
|
2237
|
+
case "notifications/resources/list_changed":
|
|
2238
|
+
await this.onResourcesListChanged();
|
|
2239
|
+
break;
|
|
2240
|
+
case "notifications/prompts/list_changed":
|
|
2241
|
+
await this.onPromptsListChanged();
|
|
2242
|
+
break;
|
|
2243
|
+
default:
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
await this.forwardNotification(notification);
|
|
2247
|
+
};
|
|
2248
|
+
const client = this.client;
|
|
2249
|
+
const handlersMap = client._notificationHandlers;
|
|
2250
|
+
for (const method of [
|
|
2251
|
+
"notifications/progress",
|
|
2252
|
+
"notifications/cancelled"
|
|
2253
|
+
]) {
|
|
2254
|
+
const originalHandler = handlersMap.get(method);
|
|
2255
|
+
if (originalHandler) {
|
|
2256
|
+
handlersMap.set(method, async (notification) => {
|
|
2257
|
+
await originalHandler(notification);
|
|
2258
|
+
await this.forwardNotification(notification);
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Forward v2 MRTR progress whose retry request IDs are not associated with
|
|
2265
|
+
* the original call callback by the current SDK beta.
|
|
2266
|
+
*
|
|
2267
|
+
* ponytail: fallback is enabled only when exactly one progress-aware call is
|
|
2268
|
+
* active; remove it when the upstream SDK propagates handlers to MRTR rounds.
|
|
2269
|
+
*/
|
|
2270
|
+
setupRoundProgressForwarding() {
|
|
2271
|
+
if (!this.client) return;
|
|
2272
|
+
const sdkClient = this.client;
|
|
2273
|
+
const original = sdkClient._onnotification.bind(this.client);
|
|
2274
|
+
sdkClient._onnotification = async (message) => {
|
|
2275
|
+
if (message && typeof message === "object" && message.method === "notifications/progress") {
|
|
2276
|
+
this.forwardRoundProgress(message.params);
|
|
2277
|
+
}
|
|
2278
|
+
await original?.(message);
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
/** Forward progress parsed from a transport stream to the active call. */
|
|
2282
|
+
forwardRoundProgress(params) {
|
|
2283
|
+
if (this.activeProgressHandlers.size === 1) {
|
|
2284
|
+
const [handler] = this.activeProgressHandlers;
|
|
2285
|
+
handler?.(
|
|
2286
|
+
params
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Auto-refresh tools cache when server sends tools/list_changed notification
|
|
2292
|
+
*/
|
|
2293
|
+
async refreshToolsCache() {
|
|
2294
|
+
if (!this.client) return;
|
|
2295
|
+
try {
|
|
2296
|
+
logger.debug(
|
|
2297
|
+
"[Auto] Refreshing tools cache due to list_changed notification"
|
|
2298
|
+
);
|
|
2299
|
+
const result = await this.client.listTools();
|
|
2300
|
+
this.toolsCache = result.tools ?? [];
|
|
2301
|
+
logger.debug(
|
|
2302
|
+
`[Auto] Refreshed tools cache: ${this.toolsCache.length} tools`
|
|
2303
|
+
);
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
logger.warn("[Auto] Failed to refresh tools cache:", err);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Called when server sends resources/list_changed notification
|
|
2310
|
+
* Resources aren't cached by default, but we log for user awareness
|
|
2311
|
+
*/
|
|
2312
|
+
async onResourcesListChanged() {
|
|
2313
|
+
logger.debug(
|
|
2314
|
+
"[Auto] Resources list changed - clients should re-fetch if needed"
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Called when server sends prompts/list_changed notification
|
|
2319
|
+
* Prompts aren't cached by default, but we log for user awareness
|
|
2320
|
+
*/
|
|
2321
|
+
async onPromptsListChanged() {
|
|
2322
|
+
logger.debug(
|
|
2323
|
+
"[Auto] Prompts list changed - clients should re-fetch if needed"
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Set roots and notify the server.
|
|
2328
|
+
* Roots represent directories or files that the client has access to.
|
|
2329
|
+
*
|
|
2330
|
+
* @param roots - Array of Root objects with `uri` (must start with "file://") and optional `name`
|
|
2331
|
+
*
|
|
2332
|
+
* @deprecated Roots are retained only for v1 compatibility.
|
|
2333
|
+
*
|
|
2334
|
+
* @example
|
|
2335
|
+
* ```typescript
|
|
2336
|
+
* await connector.setRoots([
|
|
2337
|
+
* { uri: "file:///home/user/project", name: "My Project" },
|
|
2338
|
+
* { uri: "file:///home/user/data" }
|
|
2339
|
+
* ]);
|
|
2340
|
+
* ```
|
|
2341
|
+
*/
|
|
2342
|
+
async setRoots(roots) {
|
|
2343
|
+
this.rootsCache = [...roots];
|
|
2344
|
+
if (this.client) {
|
|
2345
|
+
logger.debug(
|
|
2346
|
+
`Sending roots/list_changed notification with ${roots.length} root(s)`
|
|
2347
|
+
);
|
|
2348
|
+
await this.client.sendRootsListChanged();
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Returns the roots currently advertised to the server.
|
|
2353
|
+
*
|
|
2354
|
+
* @returns A copy of the configured roots.
|
|
2355
|
+
*/
|
|
2356
|
+
getRoots() {
|
|
2357
|
+
return [...this.rootsCache];
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Internal: set up roots/list request handler.
|
|
2361
|
+
* Must be registered after Client construction and before connect() so the
|
|
2362
|
+
* handler is available during initialize / reverse RPC for the full session.
|
|
2363
|
+
*/
|
|
2364
|
+
setupRootsHandler() {
|
|
2365
|
+
if (!this.client) return;
|
|
2366
|
+
this.client.setRequestHandler("roots/list", async () => {
|
|
2367
|
+
logger.debug(
|
|
2368
|
+
`Server requested roots list, returning ${this.rootsCache.length} root(s)`
|
|
2369
|
+
);
|
|
2370
|
+
return { roots: this.rootsCache };
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Internal: set up sampling/createMessage request handler.
|
|
2375
|
+
* Must be registered after Client construction and before connect().
|
|
2376
|
+
*/
|
|
2377
|
+
setupSamplingHandler() {
|
|
2378
|
+
if (!this.client) {
|
|
2379
|
+
logger.debug("setupSamplingHandler: No client available");
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
const samplingCallback = this.opts.onSampling;
|
|
2383
|
+
if (!samplingCallback) {
|
|
2384
|
+
logger.debug("setupSamplingHandler: No sampling callback provided");
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
logger.debug("setupSamplingHandler: Setting up sampling request handler");
|
|
2388
|
+
this.client.setRequestHandler("sampling/createMessage", async (request) => {
|
|
2389
|
+
logger.debug("Server requested sampling, forwarding to callback");
|
|
2390
|
+
return await samplingCallback(request.params);
|
|
2391
|
+
});
|
|
2392
|
+
logger.debug(
|
|
2393
|
+
"setupSamplingHandler: Sampling handler registered successfully"
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Internal: set up elicitation/create request handler.
|
|
2398
|
+
* Must be registered after Client construction and before connect().
|
|
2399
|
+
*/
|
|
2400
|
+
setupElicitationHandler() {
|
|
2401
|
+
if (!this.client) {
|
|
2402
|
+
logger.debug("setupElicitationHandler: No client available");
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const elicitationCallback = this.opts.onElicitation;
|
|
2406
|
+
if (!elicitationCallback) {
|
|
2407
|
+
logger.debug("setupElicitationHandler: No elicitation callback provided");
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
logger.debug(
|
|
2411
|
+
"setupElicitationHandler: Setting up elicitation request handler"
|
|
2412
|
+
);
|
|
2413
|
+
this.client.setRequestHandler("elicitation/create", async (request) => {
|
|
2414
|
+
logger.debug("Server requested elicitation, forwarding to callback");
|
|
2415
|
+
return await elicitationCallback(
|
|
2416
|
+
request.params
|
|
2417
|
+
);
|
|
2418
|
+
});
|
|
2419
|
+
logger.debug(
|
|
2420
|
+
"setupElicitationHandler: Elicitation handler registered successfully"
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Run one logical MCP operation. HTTP connectors override this host seam to
|
|
2425
|
+
* finish an SDK-started interactive OAuth flow and retry exactly once.
|
|
2426
|
+
*/
|
|
2427
|
+
async executeRequest(operation) {
|
|
2428
|
+
return operation();
|
|
2429
|
+
}
|
|
2430
|
+
/** OAuth state discovered for the active connection, when available. */
|
|
2431
|
+
get authorization() {
|
|
2432
|
+
return this.authorizationCache;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Discover optional authorization metadata without delaying connection
|
|
2436
|
+
* readiness. HTTP connectors override this with RFC 9728 discovery.
|
|
2437
|
+
*/
|
|
2438
|
+
async discoverAuthorization() {
|
|
2439
|
+
return this.authorization;
|
|
2440
|
+
}
|
|
2441
|
+
/** Start optional OAuth for a connected mixed-auth server. */
|
|
2442
|
+
async authenticate() {
|
|
2443
|
+
throw new Error("This connector does not support interactive OAuth");
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Disconnects the SDK client and releases transport resources.
|
|
2447
|
+
*
|
|
2448
|
+
* @returns A promise that resolves after cleanup completes.
|
|
2449
|
+
*/
|
|
2450
|
+
async disconnect() {
|
|
2451
|
+
if (!this.connected) {
|
|
2452
|
+
logger.debug("Not connected to MCP implementation");
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
logger.debug("Disconnecting from MCP implementation");
|
|
2456
|
+
await this.cleanupResources();
|
|
2457
|
+
this.connected = false;
|
|
2458
|
+
logger.debug("Disconnected from MCP implementation");
|
|
2459
|
+
}
|
|
2460
|
+
/** Whether an SDK client currently exists for this connector. */
|
|
2461
|
+
get isClientConnected() {
|
|
2462
|
+
return this.client != null;
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Initialise the MCP session **after** `connect()` has succeeded.
|
|
2466
|
+
*
|
|
2467
|
+
* In the SDK, `Client.connect(transport)` automatically performs the
|
|
2468
|
+
* protocol‑level `initialize` handshake, so we only need to cache the list of
|
|
2469
|
+
* tools and expose some server info.
|
|
2470
|
+
*
|
|
2471
|
+
* @param defaultRequestOptions - Options used while fetching the initial tool list.
|
|
2472
|
+
* @returns The capabilities advertised by the server.
|
|
2473
|
+
* @throws When {@link BaseConnector.connect} has not completed.
|
|
2474
|
+
*/
|
|
2475
|
+
async initialize(defaultRequestOptions = this.opts.defaultRequestOptions ?? {}) {
|
|
2476
|
+
if (!this.client) {
|
|
2477
|
+
throw new Error("MCP client is not connected");
|
|
2478
|
+
}
|
|
2479
|
+
logger.debug("Caching server capabilities & tools");
|
|
2480
|
+
const capabilities = this.client.getServerCapabilities();
|
|
2481
|
+
this.capabilitiesCache = capabilities || null;
|
|
2482
|
+
const serverInfo = this.client.getServerVersion();
|
|
2483
|
+
this.serverInfoCache = serverInfo ? {
|
|
2484
|
+
name: serverInfo.name,
|
|
2485
|
+
version: serverInfo.version,
|
|
2486
|
+
title: serverInfo.title,
|
|
2487
|
+
description: serverInfo.description,
|
|
2488
|
+
websiteUrl: serverInfo.websiteUrl,
|
|
2489
|
+
icons: serverInfo.icons
|
|
2490
|
+
} : null;
|
|
2491
|
+
try {
|
|
2492
|
+
const listToolsRes = await this.executeRequest(
|
|
2493
|
+
() => this.client.listTools(void 0, defaultRequestOptions)
|
|
2494
|
+
);
|
|
2495
|
+
this.toolsCache = listToolsRes.tools ?? [];
|
|
2496
|
+
logger.debug(`Fetched ${this.toolsCache.length} tools from server`);
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
if (isOAuthInteractionRequired(err)) throw err;
|
|
2499
|
+
const error = err;
|
|
2500
|
+
if (error.code === -32601) {
|
|
2501
|
+
logger.debug("Server does not implement tools/list, assuming no tools");
|
|
2502
|
+
} else {
|
|
2503
|
+
logger.debug("Failed to list tools, assuming empty:", error.message);
|
|
2504
|
+
}
|
|
2505
|
+
this.toolsCache = [];
|
|
2506
|
+
}
|
|
2507
|
+
logger.debug("Server capabilities:", capabilities);
|
|
2508
|
+
logger.debug("Server info:", serverInfo);
|
|
2509
|
+
return capabilities;
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Returns the tool list cached during initialization.
|
|
2513
|
+
*
|
|
2514
|
+
* @throws When {@link BaseConnector.initialize} has not completed.
|
|
2515
|
+
*/
|
|
2516
|
+
get tools() {
|
|
2517
|
+
if (!this.toolsCache) {
|
|
2518
|
+
throw new Error("MCP client is not initialized; call initialize() first");
|
|
2519
|
+
}
|
|
2520
|
+
return this.toolsCache;
|
|
2521
|
+
}
|
|
2522
|
+
/** Capabilities cached during initialization, or an empty object. */
|
|
2523
|
+
get serverCapabilities() {
|
|
2524
|
+
return this.capabilitiesCache || {};
|
|
2525
|
+
}
|
|
2526
|
+
/** Server identity cached during initialization, or `null`. */
|
|
2527
|
+
get serverInfo() {
|
|
2528
|
+
return this.serverInfoCache;
|
|
2529
|
+
}
|
|
2530
|
+
/** Instructions supplied by the connected server, if any. */
|
|
2531
|
+
get instructions() {
|
|
2532
|
+
return this.client?.getInstructions?.();
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* The negotiated protocol era for the active connection.
|
|
2536
|
+
* - `"legacy"` — 2025-era server, sessionful `initialize` handshake.
|
|
2537
|
+
* - `"modern"` — 2026-era server, stateless per-request.
|
|
2538
|
+
* `undefined` before the connection has negotiated.
|
|
2539
|
+
*/
|
|
2540
|
+
get protocolEra() {
|
|
2541
|
+
return this.client?.getProtocolEra?.();
|
|
2542
|
+
}
|
|
2543
|
+
/** The protocol version string negotiated for the active connection. */
|
|
2544
|
+
get negotiatedProtocolVersion() {
|
|
2545
|
+
return this.client?.getNegotiatedProtocolVersion?.();
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Calls a tool on the connected server.
|
|
2549
|
+
*
|
|
2550
|
+
* @param name - Tool name.
|
|
2551
|
+
* @param args - Tool arguments.
|
|
2552
|
+
* @param options - Per-request timeout, cancellation, and progress options.
|
|
2553
|
+
* @returns The tool result returned by the server.
|
|
2554
|
+
* @throws When the connector is not connected or the tool call fails.
|
|
2555
|
+
*/
|
|
2556
|
+
async callTool(name, args, options) {
|
|
2557
|
+
if (!this.client) {
|
|
2558
|
+
throw new Error("MCP client is not connected");
|
|
2559
|
+
}
|
|
2560
|
+
const enhancedOptions = options ? { ...options } : void 0;
|
|
2561
|
+
if (enhancedOptions?.resetTimeoutOnProgress && !enhancedOptions.onprogress) {
|
|
2562
|
+
enhancedOptions.onprogress = () => {
|
|
2563
|
+
};
|
|
2564
|
+
logger.debug(
|
|
2565
|
+
`[BaseConnector] Added onprogress callback for tool '${name}' to enable progressToken`
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
logger.debug(`Calling tool '${name}' with args`, args);
|
|
2569
|
+
const progressHandler = enhancedOptions?.onprogress;
|
|
2570
|
+
if (progressHandler) this.activeProgressHandlers.add(progressHandler);
|
|
2571
|
+
try {
|
|
2572
|
+
const res = await this.executeRequest(
|
|
2573
|
+
() => this.client.callTool({ name, arguments: args }, enhancedOptions)
|
|
2574
|
+
);
|
|
2575
|
+
logger.debug(`Tool '${name}' returned`, res);
|
|
2576
|
+
return res;
|
|
2577
|
+
} finally {
|
|
2578
|
+
if (progressHandler) this.activeProgressHandlers.delete(progressHandler);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* List all available tools from the MCP server.
|
|
2583
|
+
* This method fetches fresh tools from the server, unlike the `tools` getter which returns cached tools.
|
|
2584
|
+
*
|
|
2585
|
+
* @param options - Optional request options
|
|
2586
|
+
* @returns Array of available tools
|
|
2587
|
+
*/
|
|
2588
|
+
async listTools(options) {
|
|
2589
|
+
if (!this.client) {
|
|
2590
|
+
throw new Error("MCP client is not connected");
|
|
2591
|
+
}
|
|
2592
|
+
logger.debug("[listTools] Fetching fresh tools from server...");
|
|
2593
|
+
const result = await this.executeRequest(
|
|
2594
|
+
() => this.client.listTools(void 0, options)
|
|
2595
|
+
);
|
|
2596
|
+
const tools = result.tools ? [...result.tools] : [];
|
|
2597
|
+
logger.debug(
|
|
2598
|
+
`[listTools] Returned ${tools.length} tools:`,
|
|
2599
|
+
tools.map((t) => t.name)
|
|
2600
|
+
);
|
|
2601
|
+
return tools;
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* List resources from the server with optional pagination
|
|
2605
|
+
*
|
|
2606
|
+
* @param cursor - Optional cursor for pagination
|
|
2607
|
+
* @param options - Request options
|
|
2608
|
+
* @returns Resource list with optional nextCursor for pagination
|
|
2609
|
+
*/
|
|
2610
|
+
async listResources(cursor, options) {
|
|
2611
|
+
if (!this.client) {
|
|
2612
|
+
throw new Error("MCP client is not connected");
|
|
2613
|
+
}
|
|
2614
|
+
logger.debug("Listing resources", cursor ? `with cursor: ${cursor}` : "");
|
|
2615
|
+
return await this.executeRequest(
|
|
2616
|
+
() => this.client.listResources({ cursor }, options)
|
|
2617
|
+
);
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* List all resources from the server, automatically handling pagination
|
|
2621
|
+
*
|
|
2622
|
+
* @param options - Request options
|
|
2623
|
+
* @returns Complete list of all resources
|
|
2624
|
+
*/
|
|
2625
|
+
async listAllResources(options) {
|
|
2626
|
+
const client = this.client;
|
|
2627
|
+
if (!client) {
|
|
2628
|
+
throw new Error("MCP client is not connected");
|
|
2629
|
+
}
|
|
2630
|
+
if (!this.capabilitiesCache?.resources) {
|
|
2631
|
+
logger.debug("Server does not advertise resources capability, skipping");
|
|
2632
|
+
return { resources: [] };
|
|
2633
|
+
}
|
|
2634
|
+
try {
|
|
2635
|
+
logger.debug("Listing all resources (with auto-pagination)");
|
|
2636
|
+
return await this.executeRequest(async () => {
|
|
2637
|
+
const allResources = [];
|
|
2638
|
+
let cursor = void 0;
|
|
2639
|
+
do {
|
|
2640
|
+
const result = await client.listResources({ cursor }, options);
|
|
2641
|
+
allResources.push(...result.resources || []);
|
|
2642
|
+
cursor = result.nextCursor;
|
|
2643
|
+
} while (cursor);
|
|
2644
|
+
return { resources: allResources };
|
|
2645
|
+
});
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
const error = err;
|
|
2648
|
+
if (error.code === -32601) {
|
|
2649
|
+
logger.debug("Server advertised resources but method not found");
|
|
2650
|
+
return { resources: [] };
|
|
2651
|
+
}
|
|
2652
|
+
throw err;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* List resource templates from the server
|
|
2657
|
+
*
|
|
2658
|
+
* @param options - Request options
|
|
2659
|
+
* @returns List of available resource templates
|
|
2660
|
+
*/
|
|
2661
|
+
async listResourceTemplates(options) {
|
|
2662
|
+
if (!this.client) {
|
|
2663
|
+
throw new Error("MCP client is not connected");
|
|
2664
|
+
}
|
|
2665
|
+
logger.debug("Listing resource templates");
|
|
2666
|
+
return await this.executeRequest(
|
|
2667
|
+
() => this.client.listResourceTemplates(void 0, options)
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Request completion suggestions for a prompt or resource template argument
|
|
2672
|
+
*
|
|
2673
|
+
* @param params - Completion request parameters
|
|
2674
|
+
* @param options - Request options
|
|
2675
|
+
* @returns Completion suggestions from the server
|
|
2676
|
+
*/
|
|
2677
|
+
async complete(params, options) {
|
|
2678
|
+
if (!this.client) {
|
|
2679
|
+
throw new Error("MCP client is not connected");
|
|
2680
|
+
}
|
|
2681
|
+
logger.debug("[complete] Requesting completions for:", params.ref);
|
|
2682
|
+
const result = await this.executeRequest(
|
|
2683
|
+
() => this.client.complete(params, options)
|
|
2684
|
+
);
|
|
2685
|
+
logger.debug(
|
|
2686
|
+
`[complete] Received ${result.completion.values.length} suggestions`
|
|
2687
|
+
);
|
|
2688
|
+
return result;
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Reads a resource by URI.
|
|
2692
|
+
*
|
|
2693
|
+
* @param uri - Resource URI to read.
|
|
2694
|
+
* @param options - Per-request options.
|
|
2695
|
+
* @returns The resource contents returned by the server.
|
|
2696
|
+
*/
|
|
2697
|
+
async readResource(uri, options) {
|
|
2698
|
+
if (!this.client) {
|
|
2699
|
+
throw new Error("MCP client is not connected");
|
|
2700
|
+
}
|
|
2701
|
+
logger.debug(`Reading resource ${uri}`);
|
|
2702
|
+
const res = await this.executeRequest(
|
|
2703
|
+
() => this.client.readResource({ uri }, options)
|
|
2704
|
+
);
|
|
2705
|
+
return res;
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Subscribe to resource updates
|
|
2709
|
+
*
|
|
2710
|
+
* @param uri - URI of the resource to subscribe to
|
|
2711
|
+
* @param options - Request options
|
|
2712
|
+
*/
|
|
2713
|
+
async subscribeToResource(uri, options) {
|
|
2714
|
+
if (!this.client) {
|
|
2715
|
+
throw new Error("MCP client is not connected");
|
|
2716
|
+
}
|
|
2717
|
+
logger.debug(`Subscribing to resource: ${uri}`);
|
|
2718
|
+
return await this.executeRequest(
|
|
2719
|
+
() => this.client.subscribeResource({ uri }, options)
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Unsubscribe from resource updates
|
|
2724
|
+
*
|
|
2725
|
+
* @param uri - URI of the resource to unsubscribe from
|
|
2726
|
+
* @param options - Request options
|
|
2727
|
+
*/
|
|
2728
|
+
async unsubscribeFromResource(uri, options) {
|
|
2729
|
+
if (!this.client) {
|
|
2730
|
+
throw new Error("MCP client is not connected");
|
|
2731
|
+
}
|
|
2732
|
+
logger.debug(`Unsubscribing from resource: ${uri}`);
|
|
2733
|
+
return await this.executeRequest(
|
|
2734
|
+
() => this.client.unsubscribeResource({ uri }, options)
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Lists prompts exposed by the server.
|
|
2739
|
+
*
|
|
2740
|
+
* @returns The prompt list, or an empty list when prompts are unsupported.
|
|
2741
|
+
*/
|
|
2742
|
+
async listPrompts() {
|
|
2743
|
+
if (!this.client) {
|
|
2744
|
+
throw new Error("MCP client is not connected");
|
|
2745
|
+
}
|
|
2746
|
+
if (!this.capabilitiesCache?.prompts) {
|
|
2747
|
+
logger.debug("Server does not advertise prompts capability, skipping");
|
|
2748
|
+
return { prompts: [] };
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
logger.debug("Listing prompts");
|
|
2752
|
+
return await this.executeRequest(() => this.client.listPrompts());
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
const error = err;
|
|
2755
|
+
if (error.code === -32601) {
|
|
2756
|
+
logger.debug("Server advertised prompts but method not found");
|
|
2757
|
+
return { prompts: [] };
|
|
2758
|
+
}
|
|
2759
|
+
throw err;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Gets a prompt with the supplied arguments.
|
|
2764
|
+
*
|
|
2765
|
+
* @param name - Prompt name.
|
|
2766
|
+
* @param args - Prompt arguments.
|
|
2767
|
+
* @param options - Per-request timeout, cancellation, and progress options.
|
|
2768
|
+
* @returns The rendered prompt returned by the server.
|
|
2769
|
+
*/
|
|
2770
|
+
async getPrompt(name, args, options) {
|
|
2771
|
+
if (!this.client) {
|
|
2772
|
+
throw new Error("MCP client is not connected");
|
|
2773
|
+
}
|
|
2774
|
+
logger.debug(`Getting prompt ${name}`);
|
|
2775
|
+
return await this.executeRequest(
|
|
2776
|
+
() => this.client.getPrompt({ name, arguments: args }, options)
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Sends a raw, potentially non-standard request through the SDK client.
|
|
2781
|
+
*
|
|
2782
|
+
* @param method - JSON-RPC method name.
|
|
2783
|
+
* @param params - Request parameters. Defaults to an empty object.
|
|
2784
|
+
* @param options - Per-request options.
|
|
2785
|
+
* @returns The unvalidated result returned by the server.
|
|
2786
|
+
*/
|
|
2787
|
+
async request(method, params = null, options) {
|
|
2788
|
+
if (!this.client) {
|
|
2789
|
+
throw new Error("MCP client is not connected");
|
|
2790
|
+
}
|
|
2791
|
+
logger.debug(`Sending raw request '${method}' with params`, params);
|
|
2792
|
+
return await this.executeRequest(
|
|
2793
|
+
() => this.client.request(
|
|
2794
|
+
{ method, params: params ?? {} },
|
|
2795
|
+
passthroughResultSchema,
|
|
2796
|
+
options
|
|
2797
|
+
)
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Helper to tear down the client & connection manager safely.
|
|
2802
|
+
*/
|
|
2803
|
+
async cleanupResources() {
|
|
2804
|
+
const issues = [];
|
|
2805
|
+
if (this.client) {
|
|
2806
|
+
try {
|
|
2807
|
+
if (typeof this.client.close === "function") {
|
|
2808
|
+
await this.client.close();
|
|
2809
|
+
}
|
|
2810
|
+
} catch (e) {
|
|
2811
|
+
const msg = `Error closing client: ${e}`;
|
|
2812
|
+
logger.warn(msg);
|
|
2813
|
+
issues.push(msg);
|
|
2814
|
+
} finally {
|
|
2815
|
+
this.client = null;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (this.connectionManager) {
|
|
2819
|
+
try {
|
|
2820
|
+
await this.connectionManager.stop();
|
|
2821
|
+
} catch (e) {
|
|
2822
|
+
const msg = `Error stopping connection manager: ${e}`;
|
|
2823
|
+
logger.warn(msg);
|
|
2824
|
+
issues.push(msg);
|
|
2825
|
+
} finally {
|
|
2826
|
+
this.connectionManager = null;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
this.toolsCache = null;
|
|
2830
|
+
this.authorizationCache = void 0;
|
|
2831
|
+
if (issues.length) {
|
|
2832
|
+
logger.warn(`Resource cleanup finished with ${issues.length} issue(s)`);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
|
|
2837
|
+
// src/transport/http.ts
|
|
2838
|
+
var MIXED_AUTH_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
2839
|
+
function detectUnauthorized(err, depth = 0) {
|
|
2840
|
+
if (!err || depth > 5) return false;
|
|
2841
|
+
if (err instanceof UnauthorizedError2) return true;
|
|
2842
|
+
if (err instanceof SdkHttpError && err.status === 401) return true;
|
|
2843
|
+
if (err instanceof Error) {
|
|
2844
|
+
if (err.cause) {
|
|
2845
|
+
if (detectUnauthorized(err.cause, depth + 1)) return true;
|
|
2846
|
+
}
|
|
2847
|
+
const data = err instanceof SdkError ? err.data : void 0;
|
|
2848
|
+
if (data?.cause && detectUnauthorized(data.cause, depth + 1)) return true;
|
|
2849
|
+
}
|
|
2850
|
+
return false;
|
|
2851
|
+
}
|
|
2852
|
+
function isOAuthClientProvider(provider) {
|
|
2853
|
+
return Boolean(
|
|
2854
|
+
provider && "redirectToAuthorization" in provider && typeof provider.redirectToAuthorization === "function" && "tokens" in provider && typeof provider.tokens === "function"
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
function createMcpProxyFetch(logicalServerUrl, proxyUrl, baseFetch, serverId) {
|
|
2858
|
+
const logical = new URL(logicalServerUrl);
|
|
2859
|
+
const proxy = proxyUrl.replace(/\/$/, "");
|
|
2860
|
+
return async (input, init) => {
|
|
2861
|
+
const request = new Request(input, init);
|
|
2862
|
+
const requestUrl = new URL(request.url);
|
|
2863
|
+
const isMcpTransportRequest = requestUrl.origin === logical.origin && requestUrl.pathname === logical.pathname;
|
|
2864
|
+
if (!isMcpTransportRequest) {
|
|
2865
|
+
return baseFetch(request);
|
|
2866
|
+
}
|
|
2867
|
+
const headers = new Headers(request.headers);
|
|
2868
|
+
headers.set("X-Target-URL", request.url);
|
|
2869
|
+
if (serverId) headers.set("X-Server-Id", serverId);
|
|
2870
|
+
const body = request.method === "GET" || request.method === "HEAD" ? void 0 : await request.clone().arrayBuffer();
|
|
2871
|
+
return baseFetch(
|
|
2872
|
+
new Request(proxy, {
|
|
2873
|
+
method: request.method,
|
|
2874
|
+
headers,
|
|
2875
|
+
body,
|
|
2876
|
+
signal: request.signal,
|
|
2877
|
+
redirect: "manual"
|
|
2878
|
+
})
|
|
2879
|
+
);
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
function createDeadlineFetch(baseFetch, deadlineSignal) {
|
|
2883
|
+
return async (input, init) => {
|
|
2884
|
+
const requestSignal = init?.signal;
|
|
2885
|
+
if (!requestSignal) {
|
|
2886
|
+
return baseFetch(input, { ...init, signal: deadlineSignal });
|
|
2887
|
+
}
|
|
2888
|
+
const controller = new AbortController();
|
|
2889
|
+
const abortFromRequest = () => controller.abort(requestSignal.reason);
|
|
2890
|
+
const abortFromDeadline = () => controller.abort(deadlineSignal.reason);
|
|
2891
|
+
if (requestSignal.aborted) abortFromRequest();
|
|
2892
|
+
else
|
|
2893
|
+
requestSignal.addEventListener("abort", abortFromRequest, { once: true });
|
|
2894
|
+
if (deadlineSignal.aborted) abortFromDeadline();
|
|
2895
|
+
else
|
|
2896
|
+
deadlineSignal.addEventListener("abort", abortFromDeadline, {
|
|
2897
|
+
once: true
|
|
2898
|
+
});
|
|
2899
|
+
try {
|
|
2900
|
+
return await baseFetch(input, { ...init, signal: controller.signal });
|
|
2901
|
+
} finally {
|
|
2902
|
+
requestSignal.removeEventListener("abort", abortFromRequest);
|
|
2903
|
+
deadlineSignal.removeEventListener("abort", abortFromDeadline);
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
var HttpConnector = class extends BaseConnector {
|
|
2908
|
+
baseUrl;
|
|
2909
|
+
headers;
|
|
2910
|
+
timeout;
|
|
2911
|
+
customFetch;
|
|
2912
|
+
clientInfo;
|
|
2913
|
+
protocolNegotiation;
|
|
2914
|
+
gatewayUrl;
|
|
2915
|
+
serverId;
|
|
2916
|
+
reconnectionOptions;
|
|
2917
|
+
detectMixedAuth;
|
|
2918
|
+
transportType = null;
|
|
2919
|
+
streamableTransport = null;
|
|
2920
|
+
hadAccessTokenAtConnect = false;
|
|
2921
|
+
pendingOAuthCompletion = null;
|
|
2922
|
+
authorizationDiscovery = null;
|
|
2923
|
+
/**
|
|
2924
|
+
* Creates an HTTP connector.
|
|
2925
|
+
*
|
|
2926
|
+
* @param baseUrl - MCP endpoint URL.
|
|
2927
|
+
* @param opts - Authentication, transport, SDK, and reconnection options.
|
|
2928
|
+
*/
|
|
2929
|
+
constructor(baseUrl, opts = {}) {
|
|
2930
|
+
super(opts);
|
|
2931
|
+
const originalUrl = baseUrl.replace(/\/$/, "");
|
|
2932
|
+
this.baseUrl = originalUrl;
|
|
2933
|
+
this.headers = { ...opts.headers ?? {} };
|
|
2934
|
+
this.gatewayUrl = opts.gatewayUrl;
|
|
2935
|
+
this.serverId = opts.serverId;
|
|
2936
|
+
if (opts.authToken) {
|
|
2937
|
+
this.headers.Authorization = `Bearer ${opts.authToken}`;
|
|
2938
|
+
}
|
|
2939
|
+
this.timeout = opts.timeout ?? 1e4;
|
|
2940
|
+
const baseFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2941
|
+
this.customFetch = this.gatewayUrl ? createMcpProxyFetch(
|
|
2942
|
+
originalUrl,
|
|
2943
|
+
this.gatewayUrl,
|
|
2944
|
+
baseFetch,
|
|
2945
|
+
this.serverId
|
|
2946
|
+
) : opts.fetch;
|
|
2947
|
+
this.clientInfo = opts.clientInfo ?? {
|
|
2948
|
+
name: "http-connector",
|
|
2949
|
+
version: "1.0.0"
|
|
2950
|
+
};
|
|
2951
|
+
this.protocolNegotiation = opts.protocolNegotiation ?? "auto";
|
|
2952
|
+
this.reconnectionOptions = opts.reconnectionOptions;
|
|
2953
|
+
this.detectMixedAuth = opts.detectMixedAuth ?? true;
|
|
2954
|
+
}
|
|
2955
|
+
get oauthProvider() {
|
|
2956
|
+
return isOAuthClientProvider(this.opts.authProvider) ? this.opts.authProvider : void 0;
|
|
2957
|
+
}
|
|
2958
|
+
async completeInteractiveAuthorization() {
|
|
2959
|
+
const provider = this.oauthProvider;
|
|
2960
|
+
if (!provider) {
|
|
2961
|
+
throw new Error("No OAuth client provider is configured");
|
|
2962
|
+
}
|
|
2963
|
+
if (!this.pendingOAuthCompletion) {
|
|
2964
|
+
this.pendingOAuthCompletion = completeOAuthFlow(provider, this.baseUrl, {
|
|
2965
|
+
fetchFn: this.customFetch,
|
|
2966
|
+
finishAuthorization: async (code, iss) => {
|
|
2967
|
+
const transport = this.streamableTransport;
|
|
2968
|
+
if (!transport) {
|
|
2969
|
+
throw new Error("OAuth transport is no longer connected");
|
|
2970
|
+
}
|
|
2971
|
+
await transport.finishAuth(code, iss);
|
|
2972
|
+
}
|
|
2973
|
+
}).then(() => {
|
|
2974
|
+
this.authorizationCache = {
|
|
2975
|
+
...this.authorizationCache ?? { mode: "mixed" },
|
|
2976
|
+
authenticated: true
|
|
2977
|
+
};
|
|
2978
|
+
}).finally(() => {
|
|
2979
|
+
this.pendingOAuthCompletion = null;
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
await this.pendingOAuthCompletion;
|
|
2983
|
+
}
|
|
2984
|
+
async executeRequest(operation) {
|
|
2985
|
+
try {
|
|
2986
|
+
return await operation();
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
const provider = this.oauthProvider;
|
|
2989
|
+
if (!provider || provider.preventAutoAuth === true || !isOAuthInteractionRequired(error)) {
|
|
2990
|
+
throw error;
|
|
2991
|
+
}
|
|
2992
|
+
await this.completeInteractiveAuthorization();
|
|
2993
|
+
if (!this.connected || !this.client) {
|
|
2994
|
+
throw new Error("MCP client is not connected");
|
|
2995
|
+
}
|
|
2996
|
+
return operation();
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
/** Authenticate an already-connected server without requiring a 401 first. */
|
|
3000
|
+
async authenticate() {
|
|
3001
|
+
if (!this.connected || !this.streamableTransport) {
|
|
3002
|
+
throw new Error("MCP client is not connected");
|
|
3003
|
+
}
|
|
3004
|
+
await this.completeInteractiveAuthorization();
|
|
3005
|
+
}
|
|
3006
|
+
async discoverAuthorization() {
|
|
3007
|
+
if (!this.detectMixedAuth || !this.oauthProvider || this.hadAccessTokenAtConnect) {
|
|
3008
|
+
return this.authorizationCache;
|
|
3009
|
+
}
|
|
3010
|
+
if (this.authorizationDiscovery) return this.authorizationDiscovery;
|
|
3011
|
+
this.authorizationDiscovery = this.discoverMixedAuthorization().then(
|
|
3012
|
+
(authorization) => {
|
|
3013
|
+
if (!authorization) this.authorizationDiscovery = null;
|
|
3014
|
+
return authorization;
|
|
3015
|
+
}
|
|
3016
|
+
);
|
|
3017
|
+
return this.authorizationDiscovery;
|
|
3018
|
+
}
|
|
3019
|
+
async discoverMixedAuthorization() {
|
|
3020
|
+
const controller = new AbortController();
|
|
3021
|
+
let timeout;
|
|
3022
|
+
const discoveryTimeout = new Promise((_, reject) => {
|
|
3023
|
+
timeout = setTimeout(() => {
|
|
3024
|
+
const error = new Error(
|
|
3025
|
+
`Mixed-auth metadata discovery timed out after ${MIXED_AUTH_DISCOVERY_TIMEOUT_MS}ms`
|
|
3026
|
+
);
|
|
3027
|
+
controller.abort(error);
|
|
3028
|
+
reject(error);
|
|
3029
|
+
}, MIXED_AUTH_DISCOVERY_TIMEOUT_MS);
|
|
3030
|
+
});
|
|
3031
|
+
const baseFetch = this.customFetch ?? globalThis.fetch.bind(globalThis);
|
|
3032
|
+
try {
|
|
3033
|
+
const metadata = await Promise.race([
|
|
3034
|
+
discoverOAuthProtectedResourceMetadata(
|
|
3035
|
+
this.baseUrl,
|
|
3036
|
+
{ protocolVersion: this.negotiatedProtocolVersion },
|
|
3037
|
+
createDeadlineFetch(baseFetch, controller.signal)
|
|
3038
|
+
),
|
|
3039
|
+
discoveryTimeout
|
|
3040
|
+
]);
|
|
3041
|
+
this.authorizationCache = {
|
|
3042
|
+
mode: "mixed",
|
|
3043
|
+
authenticated: false,
|
|
3044
|
+
...metadata.resource ? { resource: metadata.resource } : {},
|
|
3045
|
+
...metadata.scopes_supported ? { scopesSupported: [...metadata.scopes_supported] } : {}
|
|
3046
|
+
};
|
|
3047
|
+
logger.info(
|
|
3048
|
+
"OAuth protected-resource metadata found after anonymous connection; server uses mixed auth"
|
|
3049
|
+
);
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
logger.debug("Mixed-auth metadata was not discovered:", error);
|
|
3052
|
+
} finally {
|
|
3053
|
+
if (timeout) clearTimeout(timeout);
|
|
3054
|
+
}
|
|
3055
|
+
return this.authorizationCache;
|
|
3056
|
+
}
|
|
3057
|
+
buildClientOptions() {
|
|
3058
|
+
return {
|
|
3059
|
+
...this.opts.clientOptions || {},
|
|
3060
|
+
jsonSchemaValidator: this.opts.clientOptions?.jsonSchemaValidator ?? new DialectJsonSchemaValidator(),
|
|
3061
|
+
versionNegotiation: {
|
|
3062
|
+
// Allow a caller-supplied versionNegotiation in clientOptions to win.
|
|
3063
|
+
mode: this.protocolNegotiation,
|
|
3064
|
+
...this.opts.clientOptions?.versionNegotiation ?? {}
|
|
3065
|
+
},
|
|
3066
|
+
listChanged: {
|
|
3067
|
+
tools: {
|
|
3068
|
+
autoRefresh: true,
|
|
3069
|
+
onChanged: (error, tools) => void this.handleListChanged(
|
|
3070
|
+
"notifications/tools/list_changed",
|
|
3071
|
+
error,
|
|
3072
|
+
tools
|
|
3073
|
+
)
|
|
3074
|
+
},
|
|
3075
|
+
resources: {
|
|
3076
|
+
autoRefresh: false,
|
|
3077
|
+
onChanged: (error) => void this.handleListChanged(
|
|
3078
|
+
"notifications/resources/list_changed",
|
|
3079
|
+
error
|
|
3080
|
+
)
|
|
3081
|
+
},
|
|
3082
|
+
prompts: {
|
|
3083
|
+
autoRefresh: false,
|
|
3084
|
+
onChanged: (error) => void this.handleListChanged(
|
|
3085
|
+
"notifications/prompts/list_changed",
|
|
3086
|
+
error
|
|
3087
|
+
)
|
|
3088
|
+
},
|
|
3089
|
+
...this.opts.clientOptions?.listChanged ?? {}
|
|
3090
|
+
},
|
|
3091
|
+
capabilities: {
|
|
3092
|
+
...this.opts.clientOptions?.capabilities || {},
|
|
3093
|
+
roots: { listChanged: true },
|
|
3094
|
+
...this.opts.onSampling ? { sampling: {} } : {},
|
|
3095
|
+
...this.opts.onElicitation ? { elicitation: { form: {}, url: {} } } : {}
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
// In v2 HTTP transport errors are thrown as SdkHttpError (subclass of
|
|
3100
|
+
// SdkError) with a numeric `.status` accessor, replacing v1's
|
|
3101
|
+
// StreamableHTTPError (which carried the status on `.code`).
|
|
3102
|
+
unwrapStreamableError(err) {
|
|
3103
|
+
if (err instanceof SdkHttpError) {
|
|
3104
|
+
return err;
|
|
3105
|
+
}
|
|
3106
|
+
if (err instanceof Error && err.cause instanceof SdkHttpError) {
|
|
3107
|
+
return err.cause;
|
|
3108
|
+
}
|
|
3109
|
+
return null;
|
|
3110
|
+
}
|
|
3111
|
+
classifyStreamableHttpFailure(err) {
|
|
3112
|
+
let fallbackReason = "Unknown error";
|
|
3113
|
+
let is401Error = false;
|
|
3114
|
+
let httpStatusCode;
|
|
3115
|
+
const streamableErr = this.unwrapStreamableError(err);
|
|
3116
|
+
if (streamableErr) {
|
|
3117
|
+
const status = streamableErr.status;
|
|
3118
|
+
is401Error = status === 401;
|
|
3119
|
+
httpStatusCode = status;
|
|
3120
|
+
if (status === 400 && streamableErr.message.includes("Missing session ID")) {
|
|
3121
|
+
fallbackReason = "Server requires session ID";
|
|
3122
|
+
logger.warn(`\u26A0\uFE0F ${fallbackReason}`);
|
|
3123
|
+
} else if (status === 404 || status === 405) {
|
|
3124
|
+
fallbackReason = `Server returned ${status} - server likely doesn't support streamable HTTP`;
|
|
3125
|
+
logger.debug(fallbackReason);
|
|
3126
|
+
} else {
|
|
3127
|
+
fallbackReason = `Server returned ${status}: ${streamableErr.message}`;
|
|
3128
|
+
logger.debug(fallbackReason);
|
|
3129
|
+
}
|
|
3130
|
+
return { fallbackReason, is401Error, httpStatusCode };
|
|
3131
|
+
}
|
|
3132
|
+
if (err instanceof Error) {
|
|
3133
|
+
const errorStr = err.toString();
|
|
3134
|
+
const errorMsg = err.message || "";
|
|
3135
|
+
is401Error = detectUnauthorized(err) || errorStr.includes("401") || errorMsg.includes("Unauthorized");
|
|
3136
|
+
if (errorStr.includes("Missing session ID") || errorStr.includes("Bad Request: Missing session ID") || errorMsg.includes("FastMCP session ID error")) {
|
|
3137
|
+
fallbackReason = "Server requires session ID";
|
|
3138
|
+
logger.warn(`\u26A0\uFE0F ${fallbackReason}`);
|
|
3139
|
+
} else if (errorStr.includes("405 Method Not Allowed") || errorStr.includes("404 Not Found")) {
|
|
3140
|
+
fallbackReason = "Server doesn't support streamable HTTP (405/404)";
|
|
3141
|
+
logger.debug(fallbackReason);
|
|
3142
|
+
} else {
|
|
3143
|
+
fallbackReason = `Streamable HTTP failed: ${err.message}`;
|
|
3144
|
+
logger.debug(fallbackReason);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
return { fallbackReason, is401Error, httpStatusCode };
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Establishes a streamable HTTP connection to the MCP server.
|
|
3151
|
+
*
|
|
3152
|
+
* @returns A promise that resolves after protocol negotiation completes.
|
|
3153
|
+
* @throws An error with `code: 401` when authentication is required.
|
|
3154
|
+
*/
|
|
3155
|
+
async connect() {
|
|
3156
|
+
if (this.connected) {
|
|
3157
|
+
logger.debug("Already connected to MCP implementation");
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
const baseUrl = this.baseUrl;
|
|
3161
|
+
logger.debug(`Connecting to MCP implementation via HTTP: ${baseUrl}`);
|
|
3162
|
+
const oauthProvider = this.oauthProvider;
|
|
3163
|
+
if (oauthProvider) {
|
|
3164
|
+
try {
|
|
3165
|
+
this.hadAccessTokenAtConnect = Boolean(
|
|
3166
|
+
(await oauthProvider.tokens())?.access_token
|
|
3167
|
+
);
|
|
3168
|
+
} catch {
|
|
3169
|
+
this.hadAccessTokenAtConnect = false;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
try {
|
|
3173
|
+
await this.connectWithStreamableHttp(baseUrl);
|
|
3174
|
+
logger.debug("\u2705 Successfully connected via streamable HTTP");
|
|
3175
|
+
} catch (err) {
|
|
3176
|
+
logger.debug("Streamable HTTP connect failed", err);
|
|
3177
|
+
const { fallbackReason, is401Error, httpStatusCode } = this.classifyStreamableHttpFailure(err);
|
|
3178
|
+
await this.cleanupResources();
|
|
3179
|
+
if (is401Error) {
|
|
3180
|
+
logger.info("Authentication required");
|
|
3181
|
+
const authError = new Error("Authentication required");
|
|
3182
|
+
authError.code = 401;
|
|
3183
|
+
throw authError;
|
|
3184
|
+
}
|
|
3185
|
+
const finalError = new Error(
|
|
3186
|
+
`Could not connect via streamable HTTP: ${fallbackReason}`
|
|
3187
|
+
);
|
|
3188
|
+
if (httpStatusCode !== void 0) {
|
|
3189
|
+
Object.defineProperty(finalError, "code", {
|
|
3190
|
+
value: httpStatusCode,
|
|
3191
|
+
writable: false,
|
|
3192
|
+
enumerable: true,
|
|
3193
|
+
configurable: true
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
throw finalError;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Tee an SSE response so v2 MRTR progress can be correlated even when the
|
|
3201
|
+
* upstream SDK does not carry the original callback to retry request IDs.
|
|
3202
|
+
*/
|
|
3203
|
+
observeSseProgress(response) {
|
|
3204
|
+
if (!response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
3205
|
+
return response;
|
|
3206
|
+
}
|
|
3207
|
+
const [body, observed] = response.body.tee();
|
|
3208
|
+
void (async () => {
|
|
3209
|
+
const reader = observed.getReader();
|
|
3210
|
+
const decoder = new TextDecoder();
|
|
3211
|
+
let buffer = "";
|
|
3212
|
+
try {
|
|
3213
|
+
while (true) {
|
|
3214
|
+
const { done, value } = await reader.read();
|
|
3215
|
+
if (done) break;
|
|
3216
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3217
|
+
const events = buffer.split(/\r?\n\r?\n/);
|
|
3218
|
+
buffer = events.pop() ?? "";
|
|
3219
|
+
for (const event of events) {
|
|
3220
|
+
for (const line of event.split(/\r?\n/)) {
|
|
3221
|
+
if (!line.startsWith("data:")) continue;
|
|
3222
|
+
try {
|
|
3223
|
+
const message = JSON.parse(line.slice(5).trim());
|
|
3224
|
+
if (message.method === "notifications/progress") {
|
|
3225
|
+
this.forwardRoundProgress(message.params);
|
|
3226
|
+
}
|
|
3227
|
+
} catch {
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
|
3234
|
+
logger.debug("Progress observer stream ended:", error);
|
|
3235
|
+
}
|
|
3236
|
+
} finally {
|
|
3237
|
+
reader.releaseLock();
|
|
3238
|
+
}
|
|
3239
|
+
})();
|
|
3240
|
+
return new Response(body, {
|
|
3241
|
+
status: response.status,
|
|
3242
|
+
statusText: response.statusText,
|
|
3243
|
+
headers: response.headers
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
async connectWithStreamableHttp(baseUrl) {
|
|
3247
|
+
try {
|
|
3248
|
+
logger.debug("[HttpConnector] Connecting with Streamable HTTP", {
|
|
3249
|
+
baseUrl,
|
|
3250
|
+
originalUrl: this.baseUrl,
|
|
3251
|
+
gatewayUrl: this.gatewayUrl || "none",
|
|
3252
|
+
authProviderUrl: this.opts.authProvider && "serverUrl" in this.opts.authProvider && typeof this.opts.authProvider.serverUrl === "string" ? this.opts.authProvider.serverUrl : "none",
|
|
3253
|
+
headers: this.headers
|
|
3254
|
+
});
|
|
3255
|
+
const baseFetch = this.customFetch ?? globalThis.fetch.bind(globalThis);
|
|
3256
|
+
const observedFetch = async (input, init) => {
|
|
3257
|
+
const response = await baseFetch(input, init);
|
|
3258
|
+
const requestHeaders = new Headers(
|
|
3259
|
+
input instanceof Request ? input.headers : void 0
|
|
3260
|
+
);
|
|
3261
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
3262
|
+
requestHeaders.set(key, value);
|
|
3263
|
+
});
|
|
3264
|
+
return requestHeaders.get("mcp-method") === "subscriptions/listen" ? response : this.observeSseProgress(response);
|
|
3265
|
+
};
|
|
3266
|
+
const streamableTransport = new StreamableHTTPClientTransport2(
|
|
3267
|
+
new URL(baseUrl),
|
|
3268
|
+
{
|
|
3269
|
+
authProvider: this.opts.authProvider,
|
|
3270
|
+
// ← Pass OAuth provider to SDK
|
|
3271
|
+
fetch: observedFetch,
|
|
3272
|
+
requestInit: {
|
|
3273
|
+
headers: this.headers
|
|
3274
|
+
},
|
|
3275
|
+
reconnectionOptions: {
|
|
3276
|
+
maxReconnectionDelay: 3e4,
|
|
3277
|
+
initialReconnectionDelay: 1e3,
|
|
3278
|
+
reconnectionDelayGrowFactor: 1.5,
|
|
3279
|
+
maxRetries: 2,
|
|
3280
|
+
...this.reconnectionOptions
|
|
3281
|
+
}
|
|
3282
|
+
// Don't pass sessionId - let the SDK generate it automatically during connect()
|
|
3283
|
+
}
|
|
3284
|
+
);
|
|
3285
|
+
let transport = streamableTransport;
|
|
3286
|
+
if (this.opts.wrapTransport) {
|
|
3287
|
+
const serverId = this.baseUrl;
|
|
3288
|
+
transport = this.opts.wrapTransport(
|
|
3289
|
+
transport,
|
|
3290
|
+
serverId
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
const clientOptions = this.buildClientOptions();
|
|
3294
|
+
logger.debug(
|
|
3295
|
+
`Creating Client with capabilities:`,
|
|
3296
|
+
JSON.stringify(clientOptions.capabilities, null, 2)
|
|
3297
|
+
);
|
|
3298
|
+
this.client = new Client(this.clientInfo, clientOptions);
|
|
3299
|
+
this.setupRootsHandler();
|
|
3300
|
+
this.setupSamplingHandler();
|
|
3301
|
+
this.setupElicitationHandler();
|
|
3302
|
+
this.setupNotificationHandler();
|
|
3303
|
+
logger.debug(
|
|
3304
|
+
"Roots/sampling/elicitation/notification handlers registered before connect"
|
|
3305
|
+
);
|
|
3306
|
+
try {
|
|
3307
|
+
let connectTimeout;
|
|
3308
|
+
await Promise.race([
|
|
3309
|
+
this.client.connect(transport),
|
|
3310
|
+
new Promise((_, reject) => {
|
|
3311
|
+
connectTimeout = setTimeout(
|
|
3312
|
+
() => reject(
|
|
3313
|
+
new Error(`MCP connection timed out after ${this.timeout}ms`)
|
|
3314
|
+
),
|
|
3315
|
+
this.timeout
|
|
3316
|
+
);
|
|
3317
|
+
})
|
|
3318
|
+
]).finally(() => {
|
|
3319
|
+
if (connectTimeout !== void 0) clearTimeout(connectTimeout);
|
|
3320
|
+
});
|
|
3321
|
+
const sessionId2 = streamableTransport.sessionId;
|
|
3322
|
+
if (sessionId2) {
|
|
3323
|
+
logger.debug(`Session ID obtained: ${sessionId2}`);
|
|
3324
|
+
}
|
|
3325
|
+
} catch (connectErr) {
|
|
3326
|
+
if (connectErr instanceof Error) {
|
|
3327
|
+
const errMsg = connectErr.message || connectErr.toString();
|
|
3328
|
+
if (errMsg.includes("Missing session ID") || errMsg.includes("Bad Request: Missing session ID") || errMsg.includes("Mcp-Session-Id header is required")) {
|
|
3329
|
+
const wrappedError = new Error(
|
|
3330
|
+
`Session ID error: ${errMsg}. The SDK should automatically extract session ID from initialize response.`
|
|
3331
|
+
);
|
|
3332
|
+
wrappedError.cause = connectErr;
|
|
3333
|
+
throw wrappedError;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
throw connectErr;
|
|
3337
|
+
}
|
|
3338
|
+
this.streamableTransport = streamableTransport;
|
|
3339
|
+
this.connectionManager = {
|
|
3340
|
+
stop: async () => {
|
|
3341
|
+
if (this.streamableTransport) {
|
|
3342
|
+
try {
|
|
3343
|
+
await this.streamableTransport.close();
|
|
3344
|
+
} catch (e) {
|
|
3345
|
+
logger.warn(`Error closing Streamable HTTP transport: ${e}`);
|
|
3346
|
+
} finally {
|
|
3347
|
+
this.streamableTransport = null;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
this.connected = true;
|
|
3353
|
+
this.transportType = "streamable-http";
|
|
3354
|
+
logger.debug(
|
|
3355
|
+
`Successfully connected to MCP implementation via streamable HTTP: ${baseUrl}`
|
|
3356
|
+
);
|
|
3357
|
+
this.trackConnectorInit({
|
|
3358
|
+
serverUrl: this.baseUrl,
|
|
3359
|
+
publicIdentifier: `${this.baseUrl} (streamable-http)`
|
|
3360
|
+
});
|
|
3361
|
+
} catch (err) {
|
|
3362
|
+
await this.cleanupResources();
|
|
3363
|
+
throw err;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Returns fields that identify the endpoint and negotiated transport.
|
|
3368
|
+
*
|
|
3369
|
+
* @returns HTTP connector identity metadata.
|
|
3370
|
+
*/
|
|
3371
|
+
get publicIdentifier() {
|
|
3372
|
+
return {
|
|
3373
|
+
type: "http",
|
|
3374
|
+
url: this.baseUrl,
|
|
3375
|
+
transport: this.transportType || "unknown",
|
|
3376
|
+
protocolEra: this.protocolEra ?? "unknown"
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Returns the active transport type.
|
|
3381
|
+
*
|
|
3382
|
+
* @returns `"streamable-http"` after connection, otherwise `null`.
|
|
3383
|
+
*/
|
|
3384
|
+
getTransportType() {
|
|
3385
|
+
return this.transportType;
|
|
3386
|
+
}
|
|
3387
|
+
// Send the streamable-HTTP DELETE *before* super.cleanupResources() invokes
|
|
3388
|
+
// client.close(). The SDK's transport.close() aborts the shared abort
|
|
3389
|
+
// controller, and terminateSession()'s DELETE fetch reuses that signal —
|
|
3390
|
+
// running it after close() rejects immediately with AbortError.
|
|
3391
|
+
async cleanupResources() {
|
|
3392
|
+
if (this.streamableTransport && this.protocolEra !== "modern") {
|
|
3393
|
+
let terminationTimeout;
|
|
3394
|
+
try {
|
|
3395
|
+
const terminated = await Promise.race([
|
|
3396
|
+
this.streamableTransport.terminateSession().then(() => true),
|
|
3397
|
+
new Promise(
|
|
3398
|
+
(resolve) => terminationTimeout = setTimeout(
|
|
3399
|
+
() => resolve(false),
|
|
3400
|
+
Math.min(this.timeout, 5e3)
|
|
3401
|
+
)
|
|
3402
|
+
)
|
|
3403
|
+
]);
|
|
3404
|
+
if (!terminated) {
|
|
3405
|
+
logger.debug(
|
|
3406
|
+
"Timed out terminating legacy HTTP session; closing transport"
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
} catch (e) {
|
|
3410
|
+
logger.debug(`Error terminating Streamable HTTP session: ${e}`);
|
|
3411
|
+
} finally {
|
|
3412
|
+
if (terminationTimeout) clearTimeout(terminationTimeout);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
await super.cleanupResources();
|
|
3416
|
+
this.authorizationDiscovery = null;
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
|
|
3420
|
+
// src/core/config.ts
|
|
3421
|
+
function resolveCallbacks(perServer, globalDefaults) {
|
|
3422
|
+
const pickSampling = perServer?.onSampling ?? globalDefaults?.onSampling;
|
|
3423
|
+
const pickElicitation = perServer?.onElicitation ?? globalDefaults?.onElicitation;
|
|
3424
|
+
const pickNotification = perServer?.onNotification ?? globalDefaults?.onNotification;
|
|
3425
|
+
return {
|
|
3426
|
+
onSampling: pickSampling,
|
|
3427
|
+
onElicitation: pickElicitation,
|
|
3428
|
+
onNotification: pickNotification
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
function shouldAutoProvisionOAuth(serverConfig) {
|
|
3432
|
+
if (!("url" in serverConfig) || typeof serverConfig.url !== "string") {
|
|
3433
|
+
return false;
|
|
3434
|
+
}
|
|
3435
|
+
if (serverConfig.authProvider) return false;
|
|
3436
|
+
if (serverConfig.authToken) return false;
|
|
3437
|
+
if (serverConfig.oauth === false) return false;
|
|
3438
|
+
const headers = serverConfig.headers;
|
|
3439
|
+
if (headers) {
|
|
3440
|
+
for (const key of Object.keys(headers)) {
|
|
3441
|
+
if (key.toLowerCase() === "authorization") return false;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
return true;
|
|
3445
|
+
}
|
|
3446
|
+
function getDefaultClientInfo() {
|
|
3447
|
+
return {
|
|
3448
|
+
name: "mcp-use",
|
|
3449
|
+
title: "mcp-use",
|
|
3450
|
+
version: getPackageVersion(),
|
|
3451
|
+
description: "mcp-use is a complete TypeScript framework for building and using MCP",
|
|
3452
|
+
icons: [
|
|
3453
|
+
{
|
|
3454
|
+
src: "https://mcp-use.com/logo.png"
|
|
3455
|
+
}
|
|
3456
|
+
],
|
|
3457
|
+
websiteUrl: "https://mcp-use.com"
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
function normalizeClientInfo(input) {
|
|
3461
|
+
const fallback = getDefaultClientInfo();
|
|
3462
|
+
if (!input || typeof input !== "object") return fallback;
|
|
3463
|
+
const ci = input;
|
|
3464
|
+
if (!ci.name || !ci.version) return fallback;
|
|
3465
|
+
return { ...fallback, ...ci };
|
|
3466
|
+
}
|
|
3467
|
+
function resolveClientOptions(clientOptions) {
|
|
3468
|
+
const capabilities = clientOptions?.capabilities;
|
|
3469
|
+
if (!capabilities || capabilities.views !== true) return clientOptions;
|
|
3470
|
+
const { views: _views, ...capsWithoutViews } = capabilities;
|
|
3471
|
+
const extensions = capsWithoutViews.extensions && typeof capsWithoutViews.extensions === "object" && !Array.isArray(capsWithoutViews.extensions) ? { ...capsWithoutViews.extensions } : {};
|
|
3472
|
+
return {
|
|
3473
|
+
...clientOptions,
|
|
3474
|
+
capabilities: {
|
|
3475
|
+
...capsWithoutViews,
|
|
3476
|
+
extensions: {
|
|
3477
|
+
...extensions,
|
|
3478
|
+
"io.modelcontextprotocol/ui": {
|
|
3479
|
+
mimeTypes: ["text/html;profile=mcp-app"]
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
function createConnectorFromConfig(serverConfig, connectorOptions) {
|
|
3486
|
+
const clientInfo = normalizeClientInfo(serverConfig.clientInfo);
|
|
3487
|
+
if ("command" in serverConfig && "args" in serverConfig) {
|
|
3488
|
+
throw new Error(
|
|
3489
|
+
"Stdio connector is not supported in this environment. Stdio connections require Node.js and are only available in the Node.js MCPClient."
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
if ("url" in serverConfig) {
|
|
3493
|
+
return new HttpConnector(serverConfig.url, {
|
|
3494
|
+
headers: serverConfig.headers,
|
|
3495
|
+
fetch: serverConfig.fetch,
|
|
3496
|
+
authToken: serverConfig.authToken,
|
|
3497
|
+
authProvider: serverConfig.authProvider,
|
|
3498
|
+
detectMixedAuth: serverConfig.detectMixedAuth,
|
|
3499
|
+
protocolNegotiation: serverConfig.protocolNegotiation,
|
|
3500
|
+
timeout: serverConfig.timeout,
|
|
3501
|
+
roots: serverConfig.roots,
|
|
3502
|
+
clientOptions: resolveClientOptions(serverConfig.clientOptions),
|
|
3503
|
+
defaultRequestOptions: serverConfig.defaultRequestOptions,
|
|
3504
|
+
clientInfo,
|
|
3505
|
+
...connectorOptions
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
throw new Error("Cannot determine connector type from config");
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// src/core/skills.ts
|
|
3512
|
+
var SKILLS_EXTENSION_ID = "io.modelcontextprotocol/skills";
|
|
3513
|
+
|
|
3514
|
+
// src/core/session.ts
|
|
3515
|
+
var MCPConnection = class {
|
|
3516
|
+
/**
|
|
3517
|
+
* The underlying connector managing the transport layer.
|
|
3518
|
+
* This is the Stdio, HTTP, or WebSocket connector handling actual communication.
|
|
3519
|
+
*/
|
|
3520
|
+
connector;
|
|
3521
|
+
/**
|
|
3522
|
+
* Whether to automatically connect when initializing.
|
|
3523
|
+
* @internal
|
|
3524
|
+
*/
|
|
3525
|
+
autoConnect;
|
|
3526
|
+
/**
|
|
3527
|
+
* Creates a new MCP session.
|
|
3528
|
+
*
|
|
3529
|
+
* @param connector - The connector to use for communication (Stdio, HTTP, WebSocket)
|
|
3530
|
+
* @param autoConnect - Whether to automatically connect during initialization (default: true)
|
|
3531
|
+
*
|
|
3532
|
+
* @example
|
|
3533
|
+
* ```typescript
|
|
3534
|
+
* const connector = new HttpConnector({ url: 'http://localhost:3000/mcp' });
|
|
3535
|
+
* const session = new MCPSession(connector);
|
|
3536
|
+
* await session.initialize(); // Auto-connects and initializes
|
|
3537
|
+
* ```
|
|
3538
|
+
*
|
|
3539
|
+
* @example
|
|
3540
|
+
* ```typescript
|
|
3541
|
+
* // Manual connection control
|
|
3542
|
+
* const session = new MCPSession(connector, false);
|
|
3543
|
+
* await session.connect();
|
|
3544
|
+
* await session.initialize();
|
|
3545
|
+
* ```
|
|
3546
|
+
*/
|
|
3547
|
+
constructor(connector, autoConnect = true) {
|
|
3548
|
+
this.connector = connector;
|
|
3549
|
+
this.autoConnect = autoConnect;
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
3552
|
+
* Establishes the connection to the MCP server.
|
|
3553
|
+
*
|
|
3554
|
+
* This method starts the underlying transport (spawns process for Stdio,
|
|
3555
|
+
* opens WebSocket, etc.) but does not perform the MCP initialization
|
|
3556
|
+
* handshake. Call {@link initialize} after connecting.
|
|
3557
|
+
*
|
|
3558
|
+
* @returns Promise that resolves when connected
|
|
3559
|
+
*
|
|
3560
|
+
* @example
|
|
3561
|
+
* ```typescript
|
|
3562
|
+
* await session.connect();
|
|
3563
|
+
* await session.initialize();
|
|
3564
|
+
* ```
|
|
3565
|
+
*
|
|
3566
|
+
* @see {@link initialize} for performing the MCP handshake
|
|
3567
|
+
* @see {@link disconnect} for closing the connection
|
|
3568
|
+
*/
|
|
3569
|
+
async connect() {
|
|
3570
|
+
await this.connector.connect();
|
|
3571
|
+
}
|
|
3572
|
+
/**
|
|
3573
|
+
* Closes the connection to the MCP server.
|
|
3574
|
+
*
|
|
3575
|
+
* This method gracefully shuts down the transport and cleans up resources.
|
|
3576
|
+
* After disconnecting, the session cannot be used until reconnected.
|
|
3577
|
+
*
|
|
3578
|
+
* @returns Promise that resolves when disconnected
|
|
3579
|
+
*
|
|
3580
|
+
* @example
|
|
3581
|
+
* ```typescript
|
|
3582
|
+
* await session.disconnect();
|
|
3583
|
+
* console.log('Session closed');
|
|
3584
|
+
* ```
|
|
3585
|
+
*
|
|
3586
|
+
* @see {@link connect} for establishing connections
|
|
3587
|
+
*/
|
|
3588
|
+
async disconnect() {
|
|
3589
|
+
await this.connector.disconnect();
|
|
3590
|
+
}
|
|
3591
|
+
/**
|
|
3592
|
+
* Initializes the MCP session with the server.
|
|
3593
|
+
*
|
|
3594
|
+
* This method performs the MCP initialization handshake, exchanging
|
|
3595
|
+
* capabilities and metadata with the server. If `autoConnect` is true
|
|
3596
|
+
* and the session is not yet connected, it will connect first.
|
|
3597
|
+
*
|
|
3598
|
+
* After initialization, you can list and call tools, read resources, etc.
|
|
3599
|
+
*
|
|
3600
|
+
* @returns Promise that resolves when initialized
|
|
3601
|
+
*
|
|
3602
|
+
* @example
|
|
3603
|
+
* ```typescript
|
|
3604
|
+
* const session = await client.createSession('my-server', false);
|
|
3605
|
+
* await session.connect();
|
|
3606
|
+
* await session.initialize();
|
|
3607
|
+
* // Now ready to use
|
|
3608
|
+
* const tools = await session.listTools();
|
|
3609
|
+
* ```
|
|
3610
|
+
*
|
|
3611
|
+
* @see {@link connect} for establishing the connection first
|
|
3612
|
+
*/
|
|
3613
|
+
async initialize() {
|
|
3614
|
+
if (!this.isConnected && this.autoConnect) {
|
|
3615
|
+
await this.connect();
|
|
3616
|
+
}
|
|
3617
|
+
await this.connector.initialize();
|
|
3618
|
+
}
|
|
3619
|
+
/**
|
|
3620
|
+
* Checks if the session is currently connected to the server.
|
|
3621
|
+
*
|
|
3622
|
+
* @returns True if connected, false otherwise
|
|
3623
|
+
*
|
|
3624
|
+
* @example
|
|
3625
|
+
* ```typescript
|
|
3626
|
+
* if (session.isConnected) {
|
|
3627
|
+
* const tools = await session.listTools();
|
|
3628
|
+
* }
|
|
3629
|
+
* ```
|
|
3630
|
+
*/
|
|
3631
|
+
get isConnected() {
|
|
3632
|
+
return this.connector && this.connector.isClientConnected;
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Register an event handler for session events
|
|
3636
|
+
*
|
|
3637
|
+
* @param event - The event type to listen for
|
|
3638
|
+
* @param handler - The handler function to call when the event occurs
|
|
3639
|
+
*
|
|
3640
|
+
* @example
|
|
3641
|
+
* ```typescript
|
|
3642
|
+
* session.on("notification", async (notification) => {
|
|
3643
|
+
* console.log(`Received: ${notification.method}`, notification.params);
|
|
3644
|
+
*
|
|
3645
|
+
* if (notification.method === "notifications/tools/list_changed") {
|
|
3646
|
+
* // Refresh tools list
|
|
3647
|
+
* }
|
|
3648
|
+
* });
|
|
3649
|
+
* ```
|
|
3650
|
+
*/
|
|
3651
|
+
on(event, handler) {
|
|
3652
|
+
if (event === "notification") {
|
|
3653
|
+
this.connector.onNotification(handler);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Set roots and notify the server.
|
|
3658
|
+
* Roots represent directories or files that the client has access to.
|
|
3659
|
+
*
|
|
3660
|
+
* @param roots - Array of Root objects with `uri` (must start with "file://") and optional `name`
|
|
3661
|
+
*
|
|
3662
|
+
* @deprecated Roots are a v1 compatibility feature and are not part of the
|
|
3663
|
+
* sessionless v2 protocol.
|
|
3664
|
+
*
|
|
3665
|
+
* @example
|
|
3666
|
+
* ```typescript
|
|
3667
|
+
* await session.setRoots([
|
|
3668
|
+
* { uri: "file:///home/user/project", name: "My Project" },
|
|
3669
|
+
* { uri: "file:///home/user/data" }
|
|
3670
|
+
* ]);
|
|
3671
|
+
* ```
|
|
3672
|
+
*/
|
|
3673
|
+
async setRoots(roots) {
|
|
3674
|
+
return this.connector.setRoots(roots);
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Gets the current roots advertised to the server.
|
|
3678
|
+
*
|
|
3679
|
+
* Roots represent directories or files that the client has provided access to.
|
|
3680
|
+
* The server may use this information to scope its operations.
|
|
3681
|
+
*
|
|
3682
|
+
* @returns Array of Root objects
|
|
3683
|
+
*
|
|
3684
|
+
* @example
|
|
3685
|
+
* ```typescript
|
|
3686
|
+
* const roots = session.getRoots();
|
|
3687
|
+
* console.log(`Current roots: ${roots.map(r => r.uri).join(', ')}`);
|
|
3688
|
+
* ```
|
|
3689
|
+
*
|
|
3690
|
+
* @see {@link setRoots} for updating roots
|
|
3691
|
+
*/
|
|
3692
|
+
getRoots() {
|
|
3693
|
+
return this.connector.getRoots();
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Get the cached list of tools from the server.
|
|
3697
|
+
*
|
|
3698
|
+
* @returns Array of available tools
|
|
3699
|
+
*
|
|
3700
|
+
* @example
|
|
3701
|
+
* ```typescript
|
|
3702
|
+
* const tools = session.tools;
|
|
3703
|
+
* console.log(`Available tools: ${tools.map(t => t.name).join(", ")}`);
|
|
3704
|
+
* ```
|
|
3705
|
+
*/
|
|
3706
|
+
get tools() {
|
|
3707
|
+
return this.connector.tools;
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* List all available tools from the MCP server.
|
|
3711
|
+
* This method fetches fresh tools from the server, unlike the `tools` getter which returns cached tools.
|
|
3712
|
+
*
|
|
3713
|
+
* @param options - Optional request options
|
|
3714
|
+
* @returns Array of available tools
|
|
3715
|
+
*
|
|
3716
|
+
* @example
|
|
3717
|
+
* ```typescript
|
|
3718
|
+
* const tools = await session.listTools();
|
|
3719
|
+
* console.log(`Available tools: ${tools.map(t => t.name).join(", ")}`);
|
|
3720
|
+
* ```
|
|
3721
|
+
*/
|
|
3722
|
+
async listTools(options) {
|
|
3723
|
+
return this.connector.listTools(options);
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Get the server capabilities advertised during initialization.
|
|
3727
|
+
*
|
|
3728
|
+
* @returns Server capabilities object
|
|
3729
|
+
*/
|
|
3730
|
+
get serverCapabilities() {
|
|
3731
|
+
return this.connector.serverCapabilities;
|
|
3732
|
+
}
|
|
3733
|
+
/**
|
|
3734
|
+
* Get the server information (name and version).
|
|
3735
|
+
*
|
|
3736
|
+
* @returns Server info object or null if not available
|
|
3737
|
+
*/
|
|
3738
|
+
get serverInfo() {
|
|
3739
|
+
return this.connector.serverInfo;
|
|
3740
|
+
}
|
|
3741
|
+
/** OAuth state discovered for this connection, when available. */
|
|
3742
|
+
get authorization() {
|
|
3743
|
+
return this.connector.authorization;
|
|
3744
|
+
}
|
|
3745
|
+
/** Discover optional OAuth metadata without delaying MCP readiness. */
|
|
3746
|
+
async discoverAuthorization() {
|
|
3747
|
+
return this.connector.discoverAuthorization();
|
|
3748
|
+
}
|
|
3749
|
+
/** Authenticate an already-connected mixed-auth server. */
|
|
3750
|
+
async authenticate() {
|
|
3751
|
+
await this.connector.authenticate();
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* The negotiated protocol era for this session's connection:
|
|
3755
|
+
* `"legacy"` (2025-era) or `"modern"` (2026-07-28-era).
|
|
3756
|
+
* `undefined` before the connection has negotiated.
|
|
3757
|
+
*/
|
|
3758
|
+
get protocolEra() {
|
|
3759
|
+
return this.connector.protocolEra;
|
|
3760
|
+
}
|
|
3761
|
+
/** The negotiated protocol version string for this session's connection. */
|
|
3762
|
+
get negotiatedProtocolVersion() {
|
|
3763
|
+
return this.connector.negotiatedProtocolVersion;
|
|
3764
|
+
}
|
|
3765
|
+
/**
|
|
3766
|
+
* Normalized server metadata for this ready connection.
|
|
3767
|
+
*
|
|
3768
|
+
* @throws When called before protocol negotiation completes.
|
|
3769
|
+
*/
|
|
3770
|
+
get info() {
|
|
3771
|
+
const protocolEra = this.protocolEra;
|
|
3772
|
+
const protocolVersion = this.negotiatedProtocolVersion;
|
|
3773
|
+
const server = this.serverInfo;
|
|
3774
|
+
if (!protocolEra || !protocolVersion) {
|
|
3775
|
+
throw new Error("MCP connection is not initialized");
|
|
3776
|
+
}
|
|
3777
|
+
const capabilities = this.serverCapabilities;
|
|
3778
|
+
const extensions = capabilities.extensions && typeof capabilities.extensions === "object" && !Array.isArray(capabilities.extensions) ? capabilities.extensions : {};
|
|
3779
|
+
return {
|
|
3780
|
+
protocolEra,
|
|
3781
|
+
protocolVersion,
|
|
3782
|
+
...server ? { server } : {},
|
|
3783
|
+
capabilities,
|
|
3784
|
+
instructions: this.connector.instructions,
|
|
3785
|
+
extensions,
|
|
3786
|
+
...this.authorization ? { authorization: this.authorization } : {}
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Whether the server advertised a named MCP capability.
|
|
3791
|
+
*
|
|
3792
|
+
* @param capability - A top-level capability name such as `"tools"` or
|
|
3793
|
+
* `"resources"`.
|
|
3794
|
+
*/
|
|
3795
|
+
supports(capability) {
|
|
3796
|
+
return capability in this.serverCapabilities;
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Call a tool on the server.
|
|
3800
|
+
*
|
|
3801
|
+
* @param name - Name of the tool to call
|
|
3802
|
+
* @param args - Arguments to pass to the tool (defaults to empty object)
|
|
3803
|
+
* @param options - Optional request options (timeout, progress handlers, etc.)
|
|
3804
|
+
* @returns Result from the tool execution
|
|
3805
|
+
*
|
|
3806
|
+
* @example
|
|
3807
|
+
* ```typescript
|
|
3808
|
+
* const result = await session.callTool("add", { a: 5, b: 3 });
|
|
3809
|
+
* console.log(`Result: ${result.content[0].text}`);
|
|
3810
|
+
* ```
|
|
3811
|
+
*/
|
|
3812
|
+
async callTool(name, args = {}, options) {
|
|
3813
|
+
return this.connector.callTool(name, args, options);
|
|
3814
|
+
}
|
|
3815
|
+
/**
|
|
3816
|
+
* List resources from the server with optional pagination.
|
|
3817
|
+
*
|
|
3818
|
+
* @param cursor - Optional cursor for pagination
|
|
3819
|
+
* @param options - Request options
|
|
3820
|
+
* @returns Resource list with optional nextCursor for pagination
|
|
3821
|
+
*
|
|
3822
|
+
* @example
|
|
3823
|
+
* ```typescript
|
|
3824
|
+
* const result = await session.listResources();
|
|
3825
|
+
* console.log(`Found ${result.resources.length} resources`);
|
|
3826
|
+
* ```
|
|
3827
|
+
*/
|
|
3828
|
+
async listResources(cursor, options) {
|
|
3829
|
+
return this.connector.listResources(cursor, options);
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* List all resources from the server, automatically handling pagination.
|
|
3833
|
+
*
|
|
3834
|
+
* @param options - Request options
|
|
3835
|
+
* @returns Complete list of all resources
|
|
3836
|
+
*
|
|
3837
|
+
* @example
|
|
3838
|
+
* ```typescript
|
|
3839
|
+
* const result = await session.listAllResources();
|
|
3840
|
+
* console.log(`Total resources: ${result.resources.length}`);
|
|
3841
|
+
* ```
|
|
3842
|
+
*/
|
|
3843
|
+
async listAllResources(options) {
|
|
3844
|
+
return this.connector.listAllResources(options);
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* List resource templates from the server.
|
|
3848
|
+
*
|
|
3849
|
+
* @param options - Request options
|
|
3850
|
+
* @returns List of available resource templates
|
|
3851
|
+
*
|
|
3852
|
+
* @example
|
|
3853
|
+
* ```typescript
|
|
3854
|
+
* const result = await session.listResourceTemplates();
|
|
3855
|
+
* console.log(`Available templates: ${result.resourceTemplates.length}`);
|
|
3856
|
+
* ```
|
|
3857
|
+
*/
|
|
3858
|
+
async listResourceTemplates(options) {
|
|
3859
|
+
return this.connector.listResourceTemplates(options);
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Request completion suggestions for a prompt or resource template argument.
|
|
3863
|
+
*
|
|
3864
|
+
* @param params - Completion request parameters
|
|
3865
|
+
* @param options - Request options
|
|
3866
|
+
* @returns Completion suggestions from the server
|
|
3867
|
+
*
|
|
3868
|
+
* @example
|
|
3869
|
+
* ```typescript
|
|
3870
|
+
* // Complete a prompt argument
|
|
3871
|
+
* const result = await session.complete({
|
|
3872
|
+
* ref: { type: "ref/prompt", name: "my-prompt" },
|
|
3873
|
+
* argument: { name: "language", value: "py" }
|
|
3874
|
+
* });
|
|
3875
|
+
* console.log(result.completion.values); // ["python"]
|
|
3876
|
+
* ```
|
|
3877
|
+
*/
|
|
3878
|
+
async complete(params, options) {
|
|
3879
|
+
return this.connector.complete(params, options);
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Read a resource by URI.
|
|
3883
|
+
*
|
|
3884
|
+
* @param uri - URI of the resource to read
|
|
3885
|
+
* @param options - Request options
|
|
3886
|
+
* @returns Resource content
|
|
3887
|
+
*
|
|
3888
|
+
* @example
|
|
3889
|
+
* ```typescript
|
|
3890
|
+
* const resource = await session.readResource("file:///path/to/file.txt");
|
|
3891
|
+
* console.log(resource.contents);
|
|
3892
|
+
* ```
|
|
3893
|
+
*/
|
|
3894
|
+
async readResource(uri, options) {
|
|
3895
|
+
return this.connector.readResource(uri, options);
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Subscribe to resource updates.
|
|
3899
|
+
*
|
|
3900
|
+
* @param uri - URI of the resource to subscribe to
|
|
3901
|
+
* @param options - Request options
|
|
3902
|
+
*
|
|
3903
|
+
* @example
|
|
3904
|
+
* ```typescript
|
|
3905
|
+
* await session.subscribeToResource("file:///path/to/file.txt");
|
|
3906
|
+
* // Now you'll receive notifications when this resource changes
|
|
3907
|
+
* ```
|
|
3908
|
+
*/
|
|
3909
|
+
async subscribeToResource(uri, options) {
|
|
3910
|
+
return this.connector.subscribeToResource(uri, options);
|
|
3911
|
+
}
|
|
3912
|
+
/**
|
|
3913
|
+
* Unsubscribe from resource updates.
|
|
3914
|
+
*
|
|
3915
|
+
* @param uri - URI of the resource to unsubscribe from
|
|
3916
|
+
* @param options - Request options
|
|
3917
|
+
*
|
|
3918
|
+
* @example
|
|
3919
|
+
* ```typescript
|
|
3920
|
+
* await session.unsubscribeFromResource("file:///path/to/file.txt");
|
|
3921
|
+
* ```
|
|
3922
|
+
*/
|
|
3923
|
+
async unsubscribeFromResource(uri, options) {
|
|
3924
|
+
return this.connector.unsubscribeFromResource(uri, options);
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* List available prompts from the server.
|
|
3928
|
+
*
|
|
3929
|
+
* @returns List of available prompts
|
|
3930
|
+
*
|
|
3931
|
+
* @example
|
|
3932
|
+
* ```typescript
|
|
3933
|
+
* const result = await session.listPrompts();
|
|
3934
|
+
* console.log(`Available prompts: ${result.prompts.length}`);
|
|
3935
|
+
* ```
|
|
3936
|
+
*/
|
|
3937
|
+
async listPrompts() {
|
|
3938
|
+
return this.connector.listPrompts();
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Get a specific prompt with arguments.
|
|
3942
|
+
*
|
|
3943
|
+
* @param name - Name of the prompt to get
|
|
3944
|
+
* @param args - Arguments for the prompt
|
|
3945
|
+
* @param options - Per-request timeout, cancellation, and progress options
|
|
3946
|
+
* @returns Prompt result
|
|
3947
|
+
*
|
|
3948
|
+
* @example
|
|
3949
|
+
* ```typescript
|
|
3950
|
+
* const prompt = await session.getPrompt("greeting", { name: "Alice" });
|
|
3951
|
+
* console.log(prompt.messages);
|
|
3952
|
+
* ```
|
|
3953
|
+
*/
|
|
3954
|
+
async getPrompt(name, args, options) {
|
|
3955
|
+
return this.connector.getPrompt(name, args, options);
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Send a raw request through the client.
|
|
3959
|
+
*
|
|
3960
|
+
* @param method - MCP method name
|
|
3961
|
+
* @param params - Request parameters
|
|
3962
|
+
* @param options - Request options
|
|
3963
|
+
* @returns Response from the server
|
|
3964
|
+
*
|
|
3965
|
+
* @example
|
|
3966
|
+
* ```typescript
|
|
3967
|
+
* const result = await session.request("custom/method", { key: "value" });
|
|
3968
|
+
* ```
|
|
3969
|
+
*/
|
|
3970
|
+
async request(method, params = null, options) {
|
|
3971
|
+
return this.connector.request(method, params, options);
|
|
3972
|
+
}
|
|
3973
|
+
/** List one page of skills advertised through the experimental extension. */
|
|
3974
|
+
async listSkills(cursor, options) {
|
|
3975
|
+
return await this.request(
|
|
3976
|
+
"skills/list",
|
|
3977
|
+
cursor === void 0 ? {} : { cursor },
|
|
3978
|
+
options
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3981
|
+
/** List the complete skill catalog, following pagination defensively. */
|
|
3982
|
+
async listAllSkills(options) {
|
|
3983
|
+
const skills = [];
|
|
3984
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3985
|
+
let cursor;
|
|
3986
|
+
do {
|
|
3987
|
+
const page = await this.listSkills(cursor, options);
|
|
3988
|
+
skills.push(...Array.isArray(page.skills) ? page.skills : []);
|
|
3989
|
+
cursor = page.nextCursor;
|
|
3990
|
+
if (cursor !== void 0) {
|
|
3991
|
+
if (seenCursors.has(cursor)) {
|
|
3992
|
+
throw new Error("skills/list returned a repeated pagination cursor");
|
|
3993
|
+
}
|
|
3994
|
+
seenCursors.add(cursor);
|
|
3995
|
+
}
|
|
3996
|
+
} while (cursor !== void 0);
|
|
3997
|
+
return { skills };
|
|
3998
|
+
}
|
|
3999
|
+
/** Get one skill by its canonical `SKILL.md` URI. */
|
|
4000
|
+
async getSkill(uri, options) {
|
|
4001
|
+
return await this.request(
|
|
4002
|
+
"skills/get",
|
|
4003
|
+
{ uri },
|
|
4004
|
+
options
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
/** Read one non-recursive skill directory. */
|
|
4008
|
+
async readResourceDirectory(uri, cursor, options) {
|
|
4009
|
+
return await this.request(
|
|
4010
|
+
"resources/directory/read",
|
|
4011
|
+
cursor === void 0 ? { uri } : { uri, cursor },
|
|
4012
|
+
options
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
|
|
4017
|
+
// src/core/base.ts
|
|
4018
|
+
function isOAuthClientProvider2(provider) {
|
|
4019
|
+
return !!provider && typeof provider === "object" && "redirectUrl" in provider && "clientMetadata" in provider;
|
|
4020
|
+
}
|
|
4021
|
+
var BaseMCPClient = class {
|
|
4022
|
+
/**
|
|
4023
|
+
* Internal configuration object containing MCP server definitions.
|
|
4024
|
+
*/
|
|
4025
|
+
config = {};
|
|
4026
|
+
/**
|
|
4027
|
+
* Map of server names to their active sessions.
|
|
4028
|
+
*/
|
|
4029
|
+
sessions = {};
|
|
4030
|
+
/**
|
|
4031
|
+
* Tracks teardown by session identity so overlapping cleanup paths share the
|
|
4032
|
+
* same disconnect. In particular, createSession() can replace a session while
|
|
4033
|
+
* closeSession() is already disconnecting it.
|
|
4034
|
+
*/
|
|
4035
|
+
sessionDisconnects = /* @__PURE__ */ new WeakMap();
|
|
4036
|
+
/**
|
|
4037
|
+
* List of server names that have active sessions.
|
|
4038
|
+
* This array is kept in sync with the sessions map and can be used
|
|
4039
|
+
* to iterate over active connections.
|
|
4040
|
+
*
|
|
4041
|
+
* @example
|
|
4042
|
+
* ```typescript
|
|
4043
|
+
* console.log(`Active servers: ${client.activeSessions.join(', ')}`);
|
|
4044
|
+
* ```
|
|
4045
|
+
*/
|
|
4046
|
+
activeSessions = [];
|
|
4047
|
+
/**
|
|
4048
|
+
* Creates a new BaseMCPClient instance.
|
|
4049
|
+
*
|
|
4050
|
+
* @param config - Optional configuration object with MCP server definitions
|
|
4051
|
+
*
|
|
4052
|
+
* @example
|
|
4053
|
+
* ```typescript
|
|
4054
|
+
* const client = new MCPClient({
|
|
4055
|
+
* mcpServers: {
|
|
4056
|
+
* 'example': {
|
|
4057
|
+
* command: 'node',
|
|
4058
|
+
* args: ['server.js']
|
|
4059
|
+
* }
|
|
4060
|
+
* }
|
|
4061
|
+
* });
|
|
4062
|
+
* ```
|
|
4063
|
+
*/
|
|
4064
|
+
constructor(config) {
|
|
4065
|
+
if (config) {
|
|
4066
|
+
this.config = config;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
disconnectSession(session) {
|
|
4070
|
+
const existingDisconnect = this.sessionDisconnects.get(session);
|
|
4071
|
+
if (existingDisconnect) {
|
|
4072
|
+
return existingDisconnect;
|
|
4073
|
+
}
|
|
4074
|
+
const disconnect = Promise.resolve().then(() => session.disconnect());
|
|
4075
|
+
this.sessionDisconnects.set(session, disconnect);
|
|
4076
|
+
return disconnect;
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Creates a client instance from a configuration dictionary.
|
|
4080
|
+
*
|
|
4081
|
+
* This static factory method must be implemented by concrete subclasses
|
|
4082
|
+
* to provide proper type information and platform-specific initialization.
|
|
4083
|
+
*
|
|
4084
|
+
* @param _cfg - Configuration dictionary
|
|
4085
|
+
* @returns Client instance
|
|
4086
|
+
* @throws If called on the base class instead of a concrete implementation
|
|
4087
|
+
*
|
|
4088
|
+
* @example
|
|
4089
|
+
* ```typescript
|
|
4090
|
+
* const client = MCPClient.fromDict({
|
|
4091
|
+
* mcpServers: {
|
|
4092
|
+
* 'my-server': { command: 'node', args: ['server.js'] }
|
|
4093
|
+
* }
|
|
4094
|
+
* });
|
|
4095
|
+
* ```
|
|
4096
|
+
*/
|
|
4097
|
+
static fromDict(_cfg) {
|
|
4098
|
+
throw new Error("fromDict must be implemented by concrete class");
|
|
4099
|
+
}
|
|
4100
|
+
/**
|
|
4101
|
+
* Adds a new MCP server configuration to the client.
|
|
4102
|
+
*
|
|
4103
|
+
* This method adds or updates a server configuration dynamically without
|
|
4104
|
+
* needing to restart the client. The server can then be used to create
|
|
4105
|
+
* new sessions.
|
|
4106
|
+
*
|
|
4107
|
+
* @param name - Unique name for the server
|
|
4108
|
+
* @param serverConfig - Server configuration object (connector type, command, args, etc.)
|
|
4109
|
+
*
|
|
4110
|
+
* @example
|
|
4111
|
+
* ```typescript
|
|
4112
|
+
* client.addServer('new-server', {
|
|
4113
|
+
* command: 'python',
|
|
4114
|
+
* args: ['server.py']
|
|
4115
|
+
* });
|
|
4116
|
+
*
|
|
4117
|
+
* // Now you can create a session
|
|
4118
|
+
* const session = await client.createSession('new-server');
|
|
4119
|
+
* ```
|
|
4120
|
+
*
|
|
4121
|
+
* @see {@link removeServer} for removing servers
|
|
4122
|
+
* @see {@link getServerConfig} for retrieving configurations
|
|
4123
|
+
*/
|
|
4124
|
+
addServer(name, serverConfig) {
|
|
4125
|
+
this.config.mcpServers = this.config.mcpServers || {};
|
|
4126
|
+
this.config.mcpServers[name] = serverConfig;
|
|
4127
|
+
trackClientAddServer(name, serverConfig);
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Removes an MCP server configuration from the client.
|
|
4131
|
+
*
|
|
4132
|
+
* This method removes a server configuration and cleans up any active
|
|
4133
|
+
* sessions associated with that server. If there's an active session,
|
|
4134
|
+
* it will be removed from the active sessions list.
|
|
4135
|
+
*
|
|
4136
|
+
* @param name - Name of the server to remove
|
|
4137
|
+
*
|
|
4138
|
+
* @example
|
|
4139
|
+
* ```typescript
|
|
4140
|
+
* // Remove a server configuration
|
|
4141
|
+
* await client.removeServer('old-server');
|
|
4142
|
+
*
|
|
4143
|
+
* // The server name will no longer appear in getServerNames()
|
|
4144
|
+
* console.log(client.getServerNames()); // 'old-server' is gone
|
|
4145
|
+
* ```
|
|
4146
|
+
*
|
|
4147
|
+
* @see {@link addServer} for adding servers
|
|
4148
|
+
* @see {@link closeSession} for properly closing sessions before removal
|
|
4149
|
+
*/
|
|
4150
|
+
async removeServer(name) {
|
|
4151
|
+
if (!this.config.mcpServers?.[name]) return;
|
|
4152
|
+
await this.closeSession(name);
|
|
4153
|
+
delete this.config.mcpServers[name];
|
|
4154
|
+
trackClientRemoveServer(name);
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Gets the names of all configured MCP servers.
|
|
4158
|
+
*
|
|
4159
|
+
* @returns Array of server names defined in the configuration
|
|
4160
|
+
*
|
|
4161
|
+
* @example
|
|
4162
|
+
* ```typescript
|
|
4163
|
+
* const serverNames = client.getServerNames();
|
|
4164
|
+
* console.log(`Configured servers: ${serverNames.join(', ')}`);
|
|
4165
|
+
*
|
|
4166
|
+
* // Create sessions for all servers
|
|
4167
|
+
* for (const name of serverNames) {
|
|
4168
|
+
* await client.createSession(name);
|
|
4169
|
+
* }
|
|
4170
|
+
* ```
|
|
4171
|
+
*
|
|
4172
|
+
* @see {@link activeSessions} for servers with active sessions
|
|
4173
|
+
*/
|
|
4174
|
+
getServerNames() {
|
|
4175
|
+
return Object.keys(this.config.mcpServers ?? {});
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* Gets the configuration for a specific MCP server.
|
|
4179
|
+
*
|
|
4180
|
+
* @param name - Name of the server
|
|
4181
|
+
* @returns Server configuration object, or undefined if not found
|
|
4182
|
+
*
|
|
4183
|
+
* @example
|
|
4184
|
+
* ```typescript
|
|
4185
|
+
* const config = client.getServerConfig('my-server');
|
|
4186
|
+
* if (config) {
|
|
4187
|
+
* console.log(`Command: ${config.command}`);
|
|
4188
|
+
* console.log(`Args: ${config.args.join(' ')}`);
|
|
4189
|
+
* }
|
|
4190
|
+
* ```
|
|
4191
|
+
*
|
|
4192
|
+
* @see {@link getConfig} for retrieving the entire configuration
|
|
4193
|
+
*/
|
|
4194
|
+
getServerConfig(name) {
|
|
4195
|
+
return this.config.mcpServers?.[name];
|
|
4196
|
+
}
|
|
4197
|
+
/**
|
|
4198
|
+
* Gets the complete client configuration.
|
|
4199
|
+
*
|
|
4200
|
+
* @returns Complete configuration object including all server definitions
|
|
4201
|
+
*
|
|
4202
|
+
* @example
|
|
4203
|
+
* ```typescript
|
|
4204
|
+
* const config = client.getConfig();
|
|
4205
|
+
* console.log(`Total servers: ${Object.keys(config.mcpServers).length}`);
|
|
4206
|
+
* ```
|
|
4207
|
+
*
|
|
4208
|
+
* @see {@link getServerConfig} for retrieving individual server configurations
|
|
4209
|
+
*/
|
|
4210
|
+
getConfig() {
|
|
4211
|
+
return this.config ?? {};
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* Creates a new session for connecting to an MCP server.
|
|
4215
|
+
*
|
|
4216
|
+
* @deprecated Use {@link connect}; modern MCP servers are sessionless.
|
|
4217
|
+
*
|
|
4218
|
+
* This method initializes a connection to the specified server using the
|
|
4219
|
+
* configuration provided during client construction. Sessions manage the
|
|
4220
|
+
* lifecycle of connections and provide methods for calling tools, listing
|
|
4221
|
+
* resources, and more.
|
|
4222
|
+
*
|
|
4223
|
+
* If a session already exists for the server, it will be replaced with a new
|
|
4224
|
+
* one and the previous session is disconnected; any previously returned
|
|
4225
|
+
* reference to it becomes unusable.
|
|
4226
|
+
*
|
|
4227
|
+
* @param serverName - The name of the server as defined in the client configuration
|
|
4228
|
+
* @param autoInitialize - Whether to automatically initialize the session (default: true)
|
|
4229
|
+
* @returns A promise that resolves to the created MCPSession instance
|
|
4230
|
+
* @throws If the server is not found in the configuration
|
|
4231
|
+
*
|
|
4232
|
+
* @example
|
|
4233
|
+
* ```typescript
|
|
4234
|
+
* // Create and initialize a session
|
|
4235
|
+
* const session = await client.createSession('my-server');
|
|
4236
|
+
* const tools = await session.listTools();
|
|
4237
|
+
*
|
|
4238
|
+
* // Create without auto-initialization
|
|
4239
|
+
* const session = await client.createSession('my-server', false);
|
|
4240
|
+
* await session.connect();
|
|
4241
|
+
* await session.initialize();
|
|
4242
|
+
* ```
|
|
4243
|
+
*
|
|
4244
|
+
* @see {@link MCPSession} for session management methods
|
|
4245
|
+
* @see {@link closeSession} for closing sessions
|
|
4246
|
+
* @see {@link getSession} for retrieving existing sessions
|
|
4247
|
+
*/
|
|
4248
|
+
async createSession(serverName, autoInitialize = true) {
|
|
4249
|
+
const servers = this.config.mcpServers ?? {};
|
|
4250
|
+
if (Object.keys(servers).length === 0) {
|
|
4251
|
+
logger.warn("No MCP servers defined in config");
|
|
4252
|
+
}
|
|
4253
|
+
if (!servers[serverName]) {
|
|
4254
|
+
throw new Error(`Server '${serverName}' not found in config`);
|
|
4255
|
+
}
|
|
4256
|
+
let serverConfig = { ...servers[serverName] };
|
|
4257
|
+
let oauthProvider;
|
|
4258
|
+
if (shouldAutoProvisionOAuth(serverConfig)) {
|
|
4259
|
+
const oauthOptions = serverConfig.oauth === false ? void 0 : serverConfig.oauth ?? {};
|
|
4260
|
+
oauthProvider = await this.createDefaultOAuthProvider(
|
|
4261
|
+
serverConfig.url,
|
|
4262
|
+
oauthOptions
|
|
4263
|
+
);
|
|
4264
|
+
serverConfig = {
|
|
4265
|
+
...serverConfig,
|
|
4266
|
+
authProvider: oauthProvider
|
|
4267
|
+
};
|
|
4268
|
+
} else if ("authProvider" in serverConfig && serverConfig.authProvider && isOAuthClientProvider2(serverConfig.authProvider)) {
|
|
4269
|
+
oauthProvider = serverConfig.authProvider;
|
|
4270
|
+
}
|
|
4271
|
+
const openSession = async () => {
|
|
4272
|
+
const connector = await Promise.resolve(
|
|
4273
|
+
this.createConnectorFromConfig(serverConfig)
|
|
4274
|
+
);
|
|
4275
|
+
const session2 = new MCPConnection(connector);
|
|
4276
|
+
if (autoInitialize) {
|
|
4277
|
+
await session2.initialize();
|
|
4278
|
+
}
|
|
4279
|
+
return session2;
|
|
4280
|
+
};
|
|
4281
|
+
let session;
|
|
4282
|
+
try {
|
|
4283
|
+
session = await openSession();
|
|
4284
|
+
} catch (err) {
|
|
4285
|
+
const httpConfig = serverConfig;
|
|
4286
|
+
if (!autoInitialize || !oauthProvider || !("url" in httpConfig) || !isUnauthorized(err)) {
|
|
4287
|
+
throw err;
|
|
4288
|
+
}
|
|
4289
|
+
if (oauthProvider.preventAutoAuth) {
|
|
4290
|
+
throw err;
|
|
4291
|
+
}
|
|
4292
|
+
logger.info(
|
|
4293
|
+
`[MCPClient] Unauthorized connecting to '${serverName}'; completing OAuth\u2026`
|
|
4294
|
+
);
|
|
4295
|
+
await completeOAuthFlow(oauthProvider, httpConfig.url);
|
|
4296
|
+
session = await openSession();
|
|
4297
|
+
}
|
|
4298
|
+
const previous = this.sessions[serverName];
|
|
4299
|
+
this.sessions[serverName] = session;
|
|
4300
|
+
if (!this.activeSessions.includes(serverName)) {
|
|
4301
|
+
this.activeSessions.push(serverName);
|
|
4302
|
+
}
|
|
4303
|
+
if (previous && previous !== session) {
|
|
4304
|
+
try {
|
|
4305
|
+
logger.debug(`Disconnecting replaced session for server ${serverName}`);
|
|
4306
|
+
await this.disconnectSession(previous);
|
|
4307
|
+
} catch (e) {
|
|
4308
|
+
logger.error(
|
|
4309
|
+
`Error disconnecting replaced session for server '${serverName}': ${e}`
|
|
4310
|
+
);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
return session;
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* Connect to a configured MCP server and return a ready, protocol-neutral
|
|
4317
|
+
* connection.
|
|
4318
|
+
*
|
|
4319
|
+
* The returned connection represents either a legacy sessionful server or a
|
|
4320
|
+
* modern sessionless server uniformly. Inspect {@link MCPConnection.info} for
|
|
4321
|
+
* the negotiated protocol version and normalized server metadata.
|
|
4322
|
+
*
|
|
4323
|
+
* @param serverName - The configured server name.
|
|
4324
|
+
*/
|
|
4325
|
+
async connect(serverName) {
|
|
4326
|
+
return this.createSession(serverName);
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* Creates sessions for all configured MCP servers.
|
|
4330
|
+
*
|
|
4331
|
+
* This is a convenience method that iterates through all servers in the
|
|
4332
|
+
* configuration and creates a session for each one. Sessions are created
|
|
4333
|
+
* sequentially to avoid overwhelming the system.
|
|
4334
|
+
*
|
|
4335
|
+
* @param autoInitialize - Whether to automatically initialize each session (default: true)
|
|
4336
|
+
* @returns A promise that resolves to a map of server names to sessions
|
|
4337
|
+
*
|
|
4338
|
+
* @example
|
|
4339
|
+
* ```typescript
|
|
4340
|
+
* // Create sessions for all configured servers
|
|
4341
|
+
* const sessions = await client.createAllSessions();
|
|
4342
|
+
* console.log(`Created ${Object.keys(sessions).length} sessions`);
|
|
4343
|
+
*
|
|
4344
|
+
* // List tools from all servers
|
|
4345
|
+
* for (const [name, session] of Object.entries(sessions)) {
|
|
4346
|
+
* const tools = await session.listTools();
|
|
4347
|
+
* console.log(`${name}: ${tools.length} tools`);
|
|
4348
|
+
* }
|
|
4349
|
+
* ```
|
|
4350
|
+
*
|
|
4351
|
+
* @see {@link createSession} for creating individual sessions
|
|
4352
|
+
* @see {@link closeAllSessions} for closing all sessions
|
|
4353
|
+
*/
|
|
4354
|
+
async createAllSessions(autoInitialize = true) {
|
|
4355
|
+
const servers = this.config.mcpServers ?? {};
|
|
4356
|
+
if (Object.keys(servers).length === 0) {
|
|
4357
|
+
logger.warn("No MCP servers defined in config");
|
|
4358
|
+
}
|
|
4359
|
+
for (const name of Object.keys(servers)) {
|
|
4360
|
+
await this.createSession(name, autoInitialize);
|
|
4361
|
+
}
|
|
4362
|
+
return this.sessions;
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Connect to every configured server sequentially.
|
|
4366
|
+
*
|
|
4367
|
+
* Each result uses the same {@link MCPConnection} API regardless of whether
|
|
4368
|
+
* the negotiated protocol is legacy/sessionful or modern/sessionless.
|
|
4369
|
+
*/
|
|
4370
|
+
async connectAll() {
|
|
4371
|
+
return this.createAllSessions();
|
|
4372
|
+
}
|
|
4373
|
+
/**
|
|
4374
|
+
* Retrieves an existing session by server name.
|
|
4375
|
+
*
|
|
4376
|
+
* This method returns null if no session exists, making it safe for
|
|
4377
|
+
* checking session existence without throwing errors.
|
|
4378
|
+
*
|
|
4379
|
+
* @param serverName - Name of the server
|
|
4380
|
+
* @returns The session instance or null if not found
|
|
4381
|
+
*
|
|
4382
|
+
* @example
|
|
4383
|
+
* ```typescript
|
|
4384
|
+
* const session = client.getSession('my-server');
|
|
4385
|
+
* if (session) {
|
|
4386
|
+
* const tools = await session.listTools();
|
|
4387
|
+
* } else {
|
|
4388
|
+
* console.log('Session not found, creating...');
|
|
4389
|
+
* await client.createSession('my-server');
|
|
4390
|
+
* }
|
|
4391
|
+
* ```
|
|
4392
|
+
*
|
|
4393
|
+
* @see {@link requireSession} for getting a session that throws if not found
|
|
4394
|
+
* @see {@link createSession} for creating sessions
|
|
4395
|
+
*/
|
|
4396
|
+
getSession(serverName) {
|
|
4397
|
+
const session = this.sessions[serverName];
|
|
4398
|
+
if (!session) {
|
|
4399
|
+
return null;
|
|
4400
|
+
}
|
|
4401
|
+
return session;
|
|
4402
|
+
}
|
|
4403
|
+
/**
|
|
4404
|
+
* Retrieves an existing session by server name, throwing if not found.
|
|
4405
|
+
*
|
|
4406
|
+
* This method is useful when you need to ensure a session exists before
|
|
4407
|
+
* proceeding. It throws a descriptive error if the session is not found.
|
|
4408
|
+
*
|
|
4409
|
+
* @param serverName - Name of the server
|
|
4410
|
+
* @returns The session instance
|
|
4411
|
+
* @throws If the session is not found
|
|
4412
|
+
*
|
|
4413
|
+
* @example
|
|
4414
|
+
* ```typescript
|
|
4415
|
+
* try {
|
|
4416
|
+
* const session = client.requireSession('my-server');
|
|
4417
|
+
* const tools = await session.listTools();
|
|
4418
|
+
* } catch (error) {
|
|
4419
|
+
* console.error('Session not found:', error.message);
|
|
4420
|
+
* }
|
|
4421
|
+
* ```
|
|
4422
|
+
*
|
|
4423
|
+
* @see {@link getSession} for a null-returning alternative
|
|
4424
|
+
* @see {@link createSession} for creating sessions
|
|
4425
|
+
*/
|
|
4426
|
+
requireSession(serverName) {
|
|
4427
|
+
const session = this.sessions[serverName];
|
|
4428
|
+
if (!session) {
|
|
4429
|
+
throw new Error(
|
|
4430
|
+
`Session '${serverName}' not found. Available sessions: ${this.activeSessions.join(", ") || "none"}`
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4433
|
+
return session;
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* Gets all active sessions as a map of server names to sessions.
|
|
4437
|
+
*
|
|
4438
|
+
* @returns Map of server names to their active sessions
|
|
4439
|
+
*
|
|
4440
|
+
* @example
|
|
4441
|
+
* ```typescript
|
|
4442
|
+
* const sessions = client.getAllActiveSessions();
|
|
4443
|
+
*
|
|
4444
|
+
* // Iterate over all active sessions
|
|
4445
|
+
* for (const [name, session] of Object.entries(sessions)) {
|
|
4446
|
+
* console.log(`Server: ${name}`);
|
|
4447
|
+
* const tools = await session.listTools();
|
|
4448
|
+
* console.log(` Tools: ${tools.length}`);
|
|
4449
|
+
* }
|
|
4450
|
+
* ```
|
|
4451
|
+
*
|
|
4452
|
+
* @see {@link activeSessions} for just the list of server names
|
|
4453
|
+
* @see {@link getSession} for retrieving individual sessions
|
|
4454
|
+
*/
|
|
4455
|
+
getAllActiveSessions() {
|
|
4456
|
+
return Object.fromEntries(
|
|
4457
|
+
this.activeSessions.map((n) => [n, this.sessions[n]])
|
|
4458
|
+
);
|
|
4459
|
+
}
|
|
4460
|
+
/**
|
|
4461
|
+
* Closes a session and cleans up its resources.
|
|
4462
|
+
*
|
|
4463
|
+
* This method gracefully disconnects from the server and removes the
|
|
4464
|
+
* session from the active sessions list. It's safe to call even if
|
|
4465
|
+
* the session doesn't exist.
|
|
4466
|
+
*
|
|
4467
|
+
* @param serverName - Name of the server whose session should be closed
|
|
4468
|
+
*
|
|
4469
|
+
* @example
|
|
4470
|
+
* ```typescript
|
|
4471
|
+
* // Close a specific session
|
|
4472
|
+
* await client.closeSession('my-server');
|
|
4473
|
+
*
|
|
4474
|
+
* // Verify it's closed
|
|
4475
|
+
* console.log(client.activeSessions.includes('my-server')); // false
|
|
4476
|
+
* ```
|
|
4477
|
+
*
|
|
4478
|
+
* @see {@link closeAllSessions} for closing all sessions at once
|
|
4479
|
+
* @see {@link createSession} for creating new sessions
|
|
4480
|
+
*/
|
|
4481
|
+
async closeSession(serverName) {
|
|
4482
|
+
const session = this.sessions[serverName];
|
|
4483
|
+
if (!session) {
|
|
4484
|
+
logger.warn(
|
|
4485
|
+
`No session exists for server ${serverName}, nothing to close`
|
|
4486
|
+
);
|
|
4487
|
+
return;
|
|
4488
|
+
}
|
|
4489
|
+
try {
|
|
4490
|
+
logger.debug(`Closing session for server ${serverName}`);
|
|
4491
|
+
await this.disconnectSession(session);
|
|
4492
|
+
} catch (e) {
|
|
4493
|
+
logger.error(`Error closing session for server '${serverName}': ${e}`);
|
|
4494
|
+
} finally {
|
|
4495
|
+
if (this.sessions[serverName] === session) {
|
|
4496
|
+
delete this.sessions[serverName];
|
|
4497
|
+
this.activeSessions = this.activeSessions.filter(
|
|
4498
|
+
(n) => n !== serverName
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
/**
|
|
4504
|
+
* Closes all active sessions and cleans up their resources.
|
|
4505
|
+
*
|
|
4506
|
+
* This method iterates through all sessions and attempts to close each one
|
|
4507
|
+
* gracefully. If any session fails to close, the error is logged but the
|
|
4508
|
+
* method continues to close remaining sessions.
|
|
4509
|
+
*
|
|
4510
|
+
* This is particularly useful for cleanup on application shutdown.
|
|
4511
|
+
*
|
|
4512
|
+
* @example
|
|
4513
|
+
* ```typescript
|
|
4514
|
+
* // Clean shutdown
|
|
4515
|
+
* try {
|
|
4516
|
+
* await client.closeAllSessions();
|
|
4517
|
+
* console.log('All sessions closed successfully');
|
|
4518
|
+
* } catch (error) {
|
|
4519
|
+
* console.error('Error during cleanup:', error);
|
|
4520
|
+
* }
|
|
4521
|
+
* ```
|
|
4522
|
+
*
|
|
4523
|
+
* @example
|
|
4524
|
+
* ```typescript
|
|
4525
|
+
* // Use in application shutdown handler
|
|
4526
|
+
* process.on('SIGINT', async () => {
|
|
4527
|
+
* console.log('Shutting down...');
|
|
4528
|
+
* await client.closeAllSessions();
|
|
4529
|
+
* process.exit(0);
|
|
4530
|
+
* });
|
|
4531
|
+
* ```
|
|
4532
|
+
*
|
|
4533
|
+
* @see {@link closeSession} for closing individual sessions
|
|
4534
|
+
* @see {@link createAllSessions} for creating sessions
|
|
4535
|
+
*/
|
|
4536
|
+
async closeAllSessions() {
|
|
4537
|
+
const serverNames = Object.keys(this.sessions);
|
|
4538
|
+
const errors = [];
|
|
4539
|
+
for (const serverName of serverNames) {
|
|
4540
|
+
try {
|
|
4541
|
+
logger.debug(`Closing session for server ${serverName}`);
|
|
4542
|
+
await this.closeSession(serverName);
|
|
4543
|
+
} catch (e) {
|
|
4544
|
+
const errorMsg = `Failed to close session for server '${serverName}': ${e}`;
|
|
4545
|
+
logger.error(errorMsg);
|
|
4546
|
+
errors.push(errorMsg);
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
if (errors.length) {
|
|
4550
|
+
logger.error(
|
|
4551
|
+
`Encountered ${errors.length} errors while closing sessions`
|
|
4552
|
+
);
|
|
4553
|
+
} else {
|
|
4554
|
+
logger.debug("All sessions closed successfully");
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
/** Close every active MCP connection. */
|
|
4558
|
+
async close() {
|
|
4559
|
+
await this.closeAllSessions();
|
|
4560
|
+
}
|
|
4561
|
+
};
|
|
4562
|
+
|
|
4563
|
+
// src/core/browser.ts
|
|
4564
|
+
function trackBrowserClientInit(config) {
|
|
4565
|
+
const servers = Object.keys(config.mcpServers ?? {});
|
|
4566
|
+
Tel.getInstance().trackMCPClientInit({
|
|
4567
|
+
codeMode: false,
|
|
4568
|
+
sandbox: false,
|
|
4569
|
+
allCallbacks: false,
|
|
4570
|
+
verify: false,
|
|
4571
|
+
servers,
|
|
4572
|
+
numServers: servers.length,
|
|
4573
|
+
isBrowser: true
|
|
4574
|
+
}).catch(
|
|
4575
|
+
(e) => logger.debug(`Failed to track BrowserMCPClient init: ${e}`)
|
|
4576
|
+
);
|
|
4577
|
+
}
|
|
4578
|
+
var BrowserMCPClient = class _BrowserMCPClient extends BaseMCPClient {
|
|
4579
|
+
/**
|
|
4580
|
+
* Returns the installed `@mcp-use/client` package version.
|
|
4581
|
+
*
|
|
4582
|
+
* @returns The package version string.
|
|
4583
|
+
*/
|
|
4584
|
+
static getPackageVersion() {
|
|
4585
|
+
return getPackageVersion();
|
|
4586
|
+
}
|
|
4587
|
+
/**
|
|
4588
|
+
* Creates a browser MCP client.
|
|
4589
|
+
*
|
|
4590
|
+
* @param config - Client configuration containing an optional `mcpServers` map.
|
|
4591
|
+
*/
|
|
4592
|
+
constructor(config) {
|
|
4593
|
+
super(config);
|
|
4594
|
+
trackBrowserClientInit(this.config);
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* Creates a browser client from an inline configuration object.
|
|
4598
|
+
*
|
|
4599
|
+
* @param cfg - Client configuration containing an optional `mcpServers` map.
|
|
4600
|
+
* @returns A browser client initialized with `cfg`.
|
|
4601
|
+
*/
|
|
4602
|
+
static fromDict(cfg) {
|
|
4603
|
+
return new _BrowserMCPClient(cfg);
|
|
4604
|
+
}
|
|
4605
|
+
async createDefaultOAuthProvider(serverUrl, options = {}) {
|
|
4606
|
+
return createOAuthProvider(serverUrl, options);
|
|
4607
|
+
}
|
|
4608
|
+
/**
|
|
4609
|
+
* Create a connector from server configuration (Browser version)
|
|
4610
|
+
* Supports HTTP connector only
|
|
4611
|
+
*/
|
|
4612
|
+
createConnectorFromConfig(serverConfig) {
|
|
4613
|
+
const {
|
|
4614
|
+
url,
|
|
4615
|
+
headers,
|
|
4616
|
+
fetch: configuredFetch,
|
|
4617
|
+
authToken,
|
|
4618
|
+
authProvider,
|
|
4619
|
+
detectMixedAuth,
|
|
4620
|
+
wrapTransport,
|
|
4621
|
+
clientOptions,
|
|
4622
|
+
protocolNegotiation,
|
|
4623
|
+
timeout,
|
|
4624
|
+
gatewayUrl,
|
|
4625
|
+
serverId,
|
|
4626
|
+
reconnectionOptions
|
|
4627
|
+
} = serverConfig;
|
|
4628
|
+
if (!url) {
|
|
4629
|
+
throw new Error("Server URL is required");
|
|
4630
|
+
}
|
|
4631
|
+
const globalDefaults = this.config;
|
|
4632
|
+
const resolved = resolveCallbacks(
|
|
4633
|
+
serverConfig,
|
|
4634
|
+
globalDefaults
|
|
4635
|
+
);
|
|
4636
|
+
const clientInfo = normalizeClientInfo(
|
|
4637
|
+
serverConfig.clientInfo ?? this.config.clientInfo
|
|
4638
|
+
);
|
|
4639
|
+
const connectorOptions = {
|
|
4640
|
+
headers,
|
|
4641
|
+
fetch: configuredFetch ?? globalThis.fetch.bind(globalThis),
|
|
4642
|
+
authToken,
|
|
4643
|
+
authProvider,
|
|
4644
|
+
detectMixedAuth,
|
|
4645
|
+
wrapTransport,
|
|
4646
|
+
clientOptions,
|
|
4647
|
+
onSampling: resolved.onSampling,
|
|
4648
|
+
onElicitation: resolved.onElicitation,
|
|
4649
|
+
onNotification: resolved.onNotification,
|
|
4650
|
+
protocolNegotiation,
|
|
4651
|
+
timeout,
|
|
4652
|
+
clientInfo,
|
|
4653
|
+
gatewayUrl,
|
|
4654
|
+
serverId,
|
|
4655
|
+
reconnectionOptions
|
|
4656
|
+
};
|
|
4657
|
+
logger.debug(
|
|
4658
|
+
`[BrowserMCPClient] Connector options prepared (clientOptions: ${clientOptions ? "provided" : "none"})`
|
|
4659
|
+
);
|
|
4660
|
+
return new HttpConnector(url, connectorOptions);
|
|
4661
|
+
}
|
|
4662
|
+
};
|
|
4663
|
+
|
|
4664
|
+
// src/utils/favicon.ts
|
|
4665
|
+
var FAVICON_API = "https://favicon.tools.mcp-use.com";
|
|
4666
|
+
var IPV4_RE = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
4667
|
+
function parseHostname(serverUrl) {
|
|
4668
|
+
try {
|
|
4669
|
+
const raw = serverUrl.includes("://") ? serverUrl : `https://${serverUrl}`;
|
|
4670
|
+
return new URL(raw).hostname;
|
|
4671
|
+
} catch {
|
|
4672
|
+
return null;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
function isLocalHost(hostname) {
|
|
4676
|
+
const h = hostname.toLowerCase();
|
|
4677
|
+
if (h === "localhost" || h.endsWith(".localhost")) return true;
|
|
4678
|
+
if (h === "host.docker.internal" || h === "0.0.0.0") return true;
|
|
4679
|
+
if (!IPV4_RE.test(h)) return false;
|
|
4680
|
+
if (h === "127.0.0.1" || h.startsWith("127.")) return true;
|
|
4681
|
+
if (h.startsWith("10.")) return true;
|
|
4682
|
+
if (h.startsWith("192.168.")) return true;
|
|
4683
|
+
const m = /^172\.(\d+)\./.exec(h);
|
|
4684
|
+
if (m) {
|
|
4685
|
+
const second = Number.parseInt(m[1], 10);
|
|
4686
|
+
if (second >= 16 && second <= 31) return true;
|
|
4687
|
+
}
|
|
4688
|
+
return false;
|
|
4689
|
+
}
|
|
4690
|
+
function subdomainLevels(hostname) {
|
|
4691
|
+
const parts = hostname.split(".");
|
|
4692
|
+
return Array.from(
|
|
4693
|
+
{ length: parts.length - 1 },
|
|
4694
|
+
(_, i) => parts.slice(i).join(".")
|
|
4695
|
+
);
|
|
4696
|
+
}
|
|
4697
|
+
function blobToDataUrl(blob) {
|
|
4698
|
+
return new Promise((resolve, reject) => {
|
|
4699
|
+
const reader = new FileReader();
|
|
4700
|
+
reader.onloadend = () => resolve(reader.result);
|
|
4701
|
+
reader.onerror = reject;
|
|
4702
|
+
reader.readAsDataURL(blob);
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
async function detectFavicon(serverUrl) {
|
|
4706
|
+
try {
|
|
4707
|
+
const hostname = parseHostname(serverUrl);
|
|
4708
|
+
if (!hostname || isLocalHost(hostname)) return null;
|
|
4709
|
+
for (const domain of subdomainLevels(hostname)) {
|
|
4710
|
+
try {
|
|
4711
|
+
const res = await fetch(`${FAVICON_API}/${domain}?response=json`, {
|
|
4712
|
+
signal: AbortSignal.timeout(2e3)
|
|
4713
|
+
});
|
|
4714
|
+
if (!res.ok) continue;
|
|
4715
|
+
const data = await res.json();
|
|
4716
|
+
if (data.source === "default") continue;
|
|
4717
|
+
const imageUrl = data.url.replace(/^http:\/\//, "https://");
|
|
4718
|
+
const img = await fetch(imageUrl, {
|
|
4719
|
+
signal: AbortSignal.timeout(2e3)
|
|
4720
|
+
});
|
|
4721
|
+
if (!img.ok) continue;
|
|
4722
|
+
return await blobToDataUrl(await img.blob());
|
|
4723
|
+
} catch {
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
return null;
|
|
4728
|
+
} catch (error) {
|
|
4729
|
+
console.warn("[favicon] Error detecting favicon:", error);
|
|
4730
|
+
return null;
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
export {
|
|
4734
|
+
BaseConnector,
|
|
4735
|
+
BrowserOAuthClientProvider,
|
|
4736
|
+
DialectJsonSchemaValidator,
|
|
4737
|
+
HttpConnector,
|
|
4738
|
+
BrowserMCPClient as MCPClient,
|
|
4739
|
+
MCPConnection,
|
|
4740
|
+
MCPConnection as MCPSession,
|
|
4741
|
+
POSTHOG_API_KEY,
|
|
4742
|
+
POSTHOG_HOST,
|
|
4743
|
+
SKILLS_EXTENSION_ID,
|
|
4744
|
+
Tel,
|
|
4745
|
+
Telemetry,
|
|
4746
|
+
UnauthorizedError3 as UnauthorizedError,
|
|
4747
|
+
VERSION,
|
|
4748
|
+
auth2 as auth,
|
|
4749
|
+
capturePostHog,
|
|
4750
|
+
completeOAuthFlow,
|
|
4751
|
+
createConnectorFromConfig,
|
|
4752
|
+
createOAuthProvider,
|
|
4753
|
+
detectFavicon,
|
|
4754
|
+
getPackageVersion,
|
|
4755
|
+
isOAuthInteractionRequired,
|
|
4756
|
+
isUnauthorized,
|
|
4757
|
+
logger,
|
|
4758
|
+
normalizeClientInfo,
|
|
4759
|
+
onMcpAuthorization,
|
|
4760
|
+
resolveCallbacks,
|
|
4761
|
+
resolveClientOptions,
|
|
4762
|
+
setProductVersion,
|
|
4763
|
+
setTelemetrySource,
|
|
4764
|
+
shouldAutoProvisionOAuth,
|
|
4765
|
+
telFetch
|
|
4766
|
+
};
|
|
4767
|
+
//# sourceMappingURL=index-browser.js.map
|