@kwonye/mcpx 0.1.100 → 0.1.102
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/{cli-28g7x9rn.js → cli-6wh8qx2y.js} +9 -3
- package/dist/{cli-dy7x0zrg.js → cli-6zq2fd6h.js} +1 -1
- package/dist/{cli-b0xrqsjc.js → cli-9k3wmag6.js} +286 -363
- package/dist/{cli-ct7asw78.js → cli-aawpvhhe.js} +13 -10
- package/dist/{cli-1ewrsfj4.js → cli-hh7rthhx.js} +4 -1
- package/dist/{cli-etmrz2c4.js → cli-qzzjkt0q.js} +1120 -715
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +352 -88
- package/dist/{config-yx5m60fv.js → config-fbgyqmv1.js} +2 -2
- package/dist/core/auth-probe.d.ts +6 -1
- package/dist/core/auth-probe.d.ts.map +1 -1
- package/dist/core/daemon.d.ts.map +1 -1
- package/dist/core/identifiers.d.ts +3 -0
- package/dist/core/identifiers.d.ts.map +1 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +22 -6
- package/dist/core/marketplace.d.ts.map +1 -1
- package/dist/core/oauth.d.ts +77 -2
- package/dist/core/oauth.d.ts.map +1 -1
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/plugin-cache.d.ts.map +1 -1
- package/dist/core/plugin-host.d.ts.map +1 -1
- package/dist/core/plugin-lifecycle.d.ts.map +1 -1
- package/dist/core/plugin-manager.d.ts.map +1 -1
- package/dist/core/plugin-parse.d.ts.map +1 -1
- package/dist/core/plugin-projections.d.ts.map +1 -1
- package/dist/core/plugin-source.d.ts.map +1 -1
- package/dist/core/secrets.d.ts.map +1 -1
- package/dist/core/skill-projections.d.ts.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/update-manager.d.ts.map +1 -1
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/{marketplace-pwabb3ea.js → marketplace-zem0m4mw.js} +5 -5
- package/dist/{plugin-host-9xcpnehq.js → plugin-host-5jmyt0td.js} +13 -39
- package/dist/{plugin-manager-19q55n69.js → plugin-manager-rw05c2x3.js} +4 -4
- package/dist/{registry-yvbxcaq6.js → registry-04kfpjh0.js} +2 -2
- package/package.json +1 -1
|
@@ -1,735 +1,1066 @@
|
|
|
1
1
|
import {
|
|
2
2
|
refreshAutoUpdateMarketplaces
|
|
3
|
-
} from "./cli-
|
|
3
|
+
} from "./cli-aawpvhhe.js";
|
|
4
4
|
import {
|
|
5
5
|
APP_VERSION,
|
|
6
6
|
getGatewayUrl,
|
|
7
|
+
mutateConfig,
|
|
7
8
|
persistSyncState,
|
|
8
9
|
syncAllClients
|
|
9
|
-
} from "./cli-
|
|
10
|
+
} from "./cli-9k3wmag6.js";
|
|
10
11
|
import {
|
|
11
12
|
loadConfig,
|
|
12
13
|
loadMergedConfig,
|
|
13
14
|
saveConfig
|
|
14
|
-
} from "./cli-
|
|
15
|
+
} from "./cli-6zq2fd6h.js";
|
|
15
16
|
import {
|
|
16
17
|
SecretsManager,
|
|
17
18
|
UpstreamError,
|
|
18
19
|
classifyUpstreamError,
|
|
19
20
|
ensureGatewayToken
|
|
20
|
-
} from "./cli-
|
|
21
|
+
} from "./cli-hh7rthhx.js";
|
|
21
22
|
import {
|
|
22
23
|
ensureDir,
|
|
23
24
|
ensureParentDir,
|
|
24
25
|
getLogPath,
|
|
25
26
|
getMarketplaceUpdateLockPath,
|
|
26
27
|
getMarketplaceUpdateStatePath,
|
|
28
|
+
getOAuthLockPath,
|
|
27
29
|
getPidPath,
|
|
28
30
|
getStagedVersionPath,
|
|
29
31
|
getUpdateLockPath,
|
|
30
32
|
getUpdatesDir
|
|
31
|
-
} from "./cli-
|
|
33
|
+
} from "./cli-6wh8qx2y.js";
|
|
32
34
|
import {
|
|
33
35
|
isServerEnabled
|
|
34
36
|
} from "./cli-s4gspqdf.js";
|
|
35
37
|
|
|
36
|
-
// src/core/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
"missing header",
|
|
54
|
-
"not authenticated",
|
|
55
|
-
"auth token"
|
|
56
|
-
];
|
|
57
|
-
function isAuthRelatedMessage(message) {
|
|
58
|
-
const lower = message.toLowerCase();
|
|
59
|
-
return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
|
|
38
|
+
// src/core/oauth.ts
|
|
39
|
+
import fs from "node:fs";
|
|
40
|
+
import path from "node:path";
|
|
41
|
+
import http from "node:http";
|
|
42
|
+
import crypto from "node:crypto";
|
|
43
|
+
import {
|
|
44
|
+
auth,
|
|
45
|
+
discoverOAuthServerInfo,
|
|
46
|
+
discoverOAuthProtectedResourceMetadata,
|
|
47
|
+
discoverAuthorizationServerMetadata,
|
|
48
|
+
refreshAuthorization
|
|
49
|
+
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
50
|
+
import { InvalidClientError, InvalidGrantError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
51
|
+
function oauthSecretName(serverName, suffix) {
|
|
52
|
+
const safe = serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_").slice(0, 32);
|
|
53
|
+
const hash = crypto.createHash("sha256").update(serverName).digest("hex").slice(0, 12);
|
|
54
|
+
return `oauth_${safe}_${hash}_${suffix}`;
|
|
60
55
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
function legacyOAuthSecretName(serverName, suffix) {
|
|
57
|
+
return `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_${suffix}`;
|
|
58
|
+
}
|
|
59
|
+
function readJsonSecret(secrets, name) {
|
|
60
|
+
let raw = secrets.getSecret(name);
|
|
61
|
+
if (!raw) {
|
|
62
|
+
const match = name.match(/^oauth_(.+)_([0-9a-f]{12})_(client|tokens|verifier|discovery)$/);
|
|
63
|
+
if (match) {
|
|
64
|
+
const suffix = match[3];
|
|
65
|
+
const withoutHash = name.replace(/_[0-9a-f]{12}_/, "_");
|
|
66
|
+
raw = secrets.getSecret(withoutHash);
|
|
67
|
+
if (!raw) {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!raw) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function getOAuthSecretWithFallback(secrets, serverName, suffix) {
|
|
80
|
+
const name = oauthSecretName(serverName, suffix);
|
|
81
|
+
let raw = secrets.getSecret(name);
|
|
82
|
+
if (raw)
|
|
83
|
+
return raw;
|
|
84
|
+
return secrets.getSecret(legacyOAuthSecretName(serverName, suffix));
|
|
85
|
+
}
|
|
86
|
+
function writeJsonSecret(secrets, name, value) {
|
|
87
|
+
secrets.setSecret(name, JSON.stringify(value));
|
|
88
|
+
const match = name.match(/^oauth_(.+)_([0-9a-f]{12})_(client|tokens|verifier|discovery)$/);
|
|
89
|
+
if (match) {
|
|
90
|
+
const withoutHash = name.replace(/_[0-9a-f]{12}_/, "_");
|
|
91
|
+
secrets.setSecret(withoutHash, JSON.stringify(value));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isOAuthReference(value) {
|
|
95
|
+
return value.startsWith("oauth://");
|
|
96
|
+
}
|
|
97
|
+
function oauthReferenceServerName(value) {
|
|
98
|
+
if (!isOAuthReference(value)) {
|
|
99
|
+
throw new Error(`Not an OAuth reference: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
return decodeURIComponent(value.slice("oauth://".length));
|
|
102
|
+
}
|
|
103
|
+
var DEFAULT_OAUTH_SUPPORT_TIMEOUT_MS = 5000;
|
|
104
|
+
var OAUTH_SUPPORT_SUPPORTED_TTL_MS = 5 * 60000;
|
|
105
|
+
var OAUTH_SUPPORT_NEGATIVE_TTL_MS = 60000;
|
|
106
|
+
var oauthSupportCache = new Map;
|
|
107
|
+
function __resetOAuthSupportCache() {
|
|
108
|
+
oauthSupportCache.clear();
|
|
109
|
+
}
|
|
110
|
+
async function probeOAuthSupport(serverUrl, options = {}) {
|
|
111
|
+
const cached = oauthSupportCache.get(serverUrl);
|
|
112
|
+
if (!options.force && cached) {
|
|
113
|
+
const ttl = cached.probe.support === "supported" ? OAUTH_SUPPORT_SUPPORTED_TTL_MS : OAUTH_SUPPORT_NEGATIVE_TTL_MS;
|
|
114
|
+
if (Date.now() - cached.at < ttl) {
|
|
115
|
+
return cached.probe;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_OAUTH_SUPPORT_TIMEOUT_MS;
|
|
119
|
+
let sawResponse = false;
|
|
120
|
+
const timeoutFetch = async (input, init) => {
|
|
121
|
+
const controller = new AbortController;
|
|
122
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(input, { ...init, signal: controller.signal });
|
|
125
|
+
sawResponse = true;
|
|
126
|
+
return response;
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
}
|
|
65
130
|
};
|
|
66
|
-
|
|
67
|
-
|
|
131
|
+
let resourceMetadataFound = false;
|
|
132
|
+
let authorizationServerUrl;
|
|
133
|
+
try {
|
|
134
|
+
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, timeoutFetch);
|
|
135
|
+
resourceMetadataFound = true;
|
|
136
|
+
authorizationServerUrl = resourceMetadata.authorization_servers?.[0];
|
|
137
|
+
} catch {}
|
|
138
|
+
let authServerMetadataFound = false;
|
|
139
|
+
if (!resourceMetadataFound) {
|
|
140
|
+
try {
|
|
141
|
+
const fallbackAuthServerUrl = new URL("/", serverUrl);
|
|
142
|
+
const metadata = await discoverAuthorizationServerMetadata(fallbackAuthServerUrl, { fetchFn: timeoutFetch });
|
|
143
|
+
authServerMetadataFound = metadata != null;
|
|
144
|
+
if (authServerMetadataFound) {
|
|
145
|
+
authorizationServerUrl = fallbackAuthServerUrl.toString();
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
68
148
|
}
|
|
69
|
-
|
|
149
|
+
let probe;
|
|
150
|
+
if (resourceMetadataFound || authServerMetadataFound) {
|
|
151
|
+
probe = {
|
|
152
|
+
support: "supported",
|
|
153
|
+
authorizationServerUrl,
|
|
154
|
+
resourceMetadata: resourceMetadataFound,
|
|
155
|
+
authorizationServerMetadata: authServerMetadataFound
|
|
156
|
+
};
|
|
157
|
+
} else if (sawResponse) {
|
|
158
|
+
probe = { support: "unsupported", resourceMetadata: false, authorizationServerMetadata: false };
|
|
159
|
+
} else {
|
|
160
|
+
probe = {
|
|
161
|
+
support: "unknown",
|
|
162
|
+
resourceMetadata: false,
|
|
163
|
+
authorizationServerMetadata: false,
|
|
164
|
+
error: "Could not reach the server to check for OAuth support."
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
oauthSupportCache.set(serverUrl, { at: Date.now(), probe });
|
|
168
|
+
return probe;
|
|
70
169
|
}
|
|
71
|
-
function
|
|
72
|
-
if (!
|
|
170
|
+
function tokensAreExpiring(tokens) {
|
|
171
|
+
if (!tokens.tokens.expires_in) {
|
|
73
172
|
return false;
|
|
74
173
|
}
|
|
75
|
-
const
|
|
76
|
-
return
|
|
174
|
+
const expiresAt = tokens.obtainedAt + tokens.tokens.expires_in * 1000;
|
|
175
|
+
return Date.now() >= expiresAt - 60000;
|
|
77
176
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
const wwwAuthenticate = response.headers.get("www-authenticate");
|
|
177
|
+
|
|
178
|
+
class McpxOAuthProvider {
|
|
179
|
+
#serverName;
|
|
180
|
+
#secrets;
|
|
181
|
+
#redirectUrlValue;
|
|
182
|
+
#stateValue;
|
|
183
|
+
constructor(serverName, secrets, redirectUrl, state = crypto.randomUUID()) {
|
|
184
|
+
this.#serverName = serverName;
|
|
185
|
+
this.#secrets = secrets;
|
|
186
|
+
this.#redirectUrlValue = redirectUrl;
|
|
187
|
+
this.#stateValue = state;
|
|
188
|
+
}
|
|
189
|
+
get redirectUrl() {
|
|
190
|
+
return this.#redirectUrlValue;
|
|
191
|
+
}
|
|
192
|
+
get clientMetadata() {
|
|
193
|
+
if (!this.#redirectUrlValue) {
|
|
96
194
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
195
|
+
redirect_uris: [],
|
|
196
|
+
token_endpoint_auth_method: "none",
|
|
197
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
198
|
+
response_types: ["code"],
|
|
199
|
+
client_name: "mcpx"
|
|
101
200
|
};
|
|
102
201
|
}
|
|
103
|
-
if (response.status === 200) {
|
|
104
|
-
try {
|
|
105
|
-
const body = await response.json();
|
|
106
|
-
const err = body?.error;
|
|
107
|
-
if (err && typeof err === "object" && !("result" in body)) {
|
|
108
|
-
const message = typeof err.message === "string" ? err.message : "";
|
|
109
|
-
if (message && isAuthRelatedMessage(message)) {
|
|
110
|
-
return {
|
|
111
|
-
authRequired: true,
|
|
112
|
-
status: response.status,
|
|
113
|
-
error: `JSON-RPC error: ${message}`
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} catch {}
|
|
118
|
-
}
|
|
119
202
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
authRequired: false,
|
|
127
|
-
error: isAbort ? `Auth probe timed out after ${timeoutMs}ms.` : error.message
|
|
203
|
+
redirect_uris: [this.#redirectUrlValue],
|
|
204
|
+
token_endpoint_auth_method: "none",
|
|
205
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
206
|
+
response_types: ["code"],
|
|
207
|
+
client_name: "mcpx"
|
|
128
208
|
};
|
|
129
|
-
} finally {
|
|
130
|
-
clearTimeout(timeoutHandle);
|
|
131
209
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// src/core/registry-client.ts
|
|
135
|
-
var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
|
|
136
|
-
async function fetchRegistryServerDetail(name) {
|
|
137
|
-
const encoded = encodeURIComponent(name);
|
|
138
|
-
const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
|
|
139
|
-
headers: { accept: "application/json" }
|
|
140
|
-
});
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
throw new Error(`Registry API error: ${response.status}`);
|
|
210
|
+
state() {
|
|
211
|
+
return this.#stateValue;
|
|
143
212
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
var PACKAGE_PRIORITY = {
|
|
148
|
-
npm: 1,
|
|
149
|
-
pypi: 2,
|
|
150
|
-
nuget: 3,
|
|
151
|
-
oci: 4,
|
|
152
|
-
mcpb: 5
|
|
153
|
-
};
|
|
154
|
-
var RUNTIME_HINT = {
|
|
155
|
-
npm: "npx",
|
|
156
|
-
pypi: "uvx",
|
|
157
|
-
nuget: "dnx"
|
|
158
|
-
};
|
|
159
|
-
function selectBestPackage(packages = [], remotes = []) {
|
|
160
|
-
const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
|
|
161
|
-
if (stdioPkgs.length > 0) {
|
|
162
|
-
return { kind: "package", package: stdioPkgs[0] };
|
|
213
|
+
clientInformation() {
|
|
214
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"));
|
|
163
215
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return { kind: "remote", remote: httpRemotes[0] };
|
|
216
|
+
saveClientInformation(clientInformation) {
|
|
217
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "client"), clientInformation);
|
|
167
218
|
}
|
|
168
|
-
|
|
169
|
-
return
|
|
219
|
+
tokens() {
|
|
220
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"))?.tokens;
|
|
170
221
|
}
|
|
171
|
-
|
|
172
|
-
|
|
222
|
+
saveTokens(tokens) {
|
|
223
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
|
|
224
|
+
tokens,
|
|
225
|
+
obtainedAt: Date.now()
|
|
226
|
+
});
|
|
173
227
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
name: env.name,
|
|
183
|
-
description: env.description,
|
|
184
|
-
isSecret: env.isSecret ?? false,
|
|
185
|
-
kind: "env"
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
for (const arg of option.package.packageArguments ?? []) {
|
|
190
|
-
if (arg.isRequired && !arg.value && !arg.default) {
|
|
191
|
-
inputs.push({
|
|
192
|
-
name: arg.name ?? arg.valueHint ?? "arg",
|
|
193
|
-
description: arg.description,
|
|
194
|
-
isSecret: false,
|
|
195
|
-
kind: "arg"
|
|
196
|
-
});
|
|
197
|
-
}
|
|
228
|
+
redirectToAuthorization(_authorizationUrl) {}
|
|
229
|
+
saveCodeVerifier(codeVerifier) {
|
|
230
|
+
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
231
|
+
}
|
|
232
|
+
codeVerifier() {
|
|
233
|
+
const verifier = getOAuthSecretWithFallback(this.#secrets, this.#serverName, "verifier");
|
|
234
|
+
if (!verifier) {
|
|
235
|
+
throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
|
|
198
236
|
}
|
|
237
|
+
return verifier;
|
|
199
238
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
saveDiscoveryState(state) {
|
|
240
|
+
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
|
|
241
|
+
}
|
|
242
|
+
discoveryState() {
|
|
243
|
+
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
|
|
244
|
+
}
|
|
245
|
+
invalidateCredentials(scope) {
|
|
246
|
+
const suffixes = scope === "all" ? ["client", "tokens", "verifier", "discovery"] : [scope];
|
|
247
|
+
for (const suffix of suffixes) {
|
|
248
|
+
this.#secrets.removeSecret(oauthSecretName(this.#serverName, suffix));
|
|
249
|
+
this.#secrets.removeSecret(legacyOAuthSecretName(this.#serverName, suffix));
|
|
210
250
|
}
|
|
211
251
|
}
|
|
212
|
-
return inputs;
|
|
213
252
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
transport: "http",
|
|
224
|
-
url: option.remote.url,
|
|
225
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
226
|
-
};
|
|
253
|
+
|
|
254
|
+
class InteractiveOAuthProvider extends McpxOAuthProvider {
|
|
255
|
+
#openUrl;
|
|
256
|
+
#cachedClientInfo;
|
|
257
|
+
#cachedCodeVerifier;
|
|
258
|
+
constructor(serverName, secrets, redirectUrl, state, openUrl) {
|
|
259
|
+
super(serverName, secrets, redirectUrl, state);
|
|
260
|
+
this.#openUrl = openUrl;
|
|
227
261
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
263
|
+
await this.#openUrl(authorizationUrl.toString());
|
|
264
|
+
}
|
|
265
|
+
clientInformation() {
|
|
266
|
+
this.#cachedClientInfo ??= super.clientInformation();
|
|
267
|
+
return this.#cachedClientInfo;
|
|
268
|
+
}
|
|
269
|
+
saveClientInformation(clientInformation) {
|
|
270
|
+
super.saveClientInformation(clientInformation);
|
|
271
|
+
this.#cachedClientInfo = clientInformation;
|
|
272
|
+
}
|
|
273
|
+
codeVerifier() {
|
|
274
|
+
if (this.#cachedCodeVerifier !== undefined) {
|
|
275
|
+
return this.#cachedCodeVerifier;
|
|
241
276
|
}
|
|
277
|
+
return super.codeVerifier();
|
|
242
278
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
279
|
+
saveCodeVerifier(codeVerifier) {
|
|
280
|
+
super.saveCodeVerifier(codeVerifier);
|
|
281
|
+
this.#cachedCodeVerifier = codeVerifier;
|
|
282
|
+
}
|
|
283
|
+
invalidateCredentials(scope) {
|
|
284
|
+
super.invalidateCredentials(scope);
|
|
285
|
+
if (scope === "all" || scope === "client") {
|
|
286
|
+
this.#cachedClientInfo = undefined;
|
|
287
|
+
}
|
|
248
288
|
}
|
|
249
|
-
return {
|
|
250
|
-
transport: "stdio",
|
|
251
|
-
command: runtime,
|
|
252
|
-
args,
|
|
253
|
-
...Object.keys(env).length > 0 ? { env } : {}
|
|
254
|
-
};
|
|
255
289
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
function listen(server) {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
server.once("error", reject);
|
|
293
|
+
server.listen(0, "127.0.0.1", () => {
|
|
294
|
+
const address = server.address();
|
|
295
|
+
if (!address || typeof address === "string") {
|
|
296
|
+
reject(new Error("Failed to start OAuth callback server."));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
resolve(address.port);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
260
302
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return normalized;
|
|
303
|
+
function closeServer(server) {
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
server.close(() => resolve());
|
|
306
|
+
});
|
|
267
307
|
}
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
var OAUTH_LOGIN_TIMEOUT_MS = 120000;
|
|
309
|
+
function renderCallbackPage(kind, detail) {
|
|
310
|
+
const title = kind === "success" ? "Signed in" : "Sign-in failed";
|
|
311
|
+
const message = kind === "success" ? "You're signed in. You can close this window and return to mcpx." : detail ?? "Something went wrong. You can close this window and try again in mcpx.";
|
|
312
|
+
const icon = kind === "success" ? "✓" : "✗";
|
|
313
|
+
const closeScript = kind === "success" ? "<script>setTimeout(() => window.close(), 1500);</script>" : "";
|
|
314
|
+
return `<!doctype html>
|
|
315
|
+
<html>
|
|
316
|
+
<head>
|
|
317
|
+
<meta charset="utf-8">
|
|
318
|
+
<title>${title} — mcpx</title>
|
|
319
|
+
<style>
|
|
320
|
+
:root { color-scheme: light dark; }
|
|
321
|
+
body {
|
|
322
|
+
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
323
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
324
|
+
background: #f5f5f7; color: #1d1d1f;
|
|
325
|
+
}
|
|
326
|
+
@media (prefers-color-scheme: dark) {
|
|
327
|
+
body { background: #1c1c1e; color: #f5f5f7; }
|
|
328
|
+
.card { background: #2c2c2e !important; box-shadow: none !important; }
|
|
329
|
+
}
|
|
330
|
+
.card {
|
|
331
|
+
background: #fff; border-radius: 16px; padding: 40px 48px; text-align: center;
|
|
332
|
+
box-shadow: 0 8px 30px rgba(0,0,0,0.12); max-width: 360px;
|
|
333
|
+
}
|
|
334
|
+
.icon {
|
|
335
|
+
width: 48px; height: 48px; border-radius: 50%; margin: 0 auto 16px;
|
|
336
|
+
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
|
337
|
+
background: ${kind === "success" ? "#34c759" : "#ff3b30"}; color: #fff;
|
|
338
|
+
}
|
|
339
|
+
h1 { font-size: 17px; margin: 0 0 8px; }
|
|
340
|
+
p { font-size: 14px; margin: 0; opacity: 0.7; }
|
|
341
|
+
</style>
|
|
342
|
+
</head>
|
|
343
|
+
<body>
|
|
344
|
+
<div class="card">
|
|
345
|
+
<div class="icon">${icon}</div>
|
|
346
|
+
<h1>${title}</h1>
|
|
347
|
+
<p>${message}</p>
|
|
348
|
+
</div>
|
|
349
|
+
${closeScript}
|
|
350
|
+
</body>
|
|
351
|
+
</html>`;
|
|
270
352
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
353
|
+
|
|
354
|
+
class OAuthCancelledError extends Error {
|
|
355
|
+
code = "oauth_cancelled";
|
|
356
|
+
constructor(serverName) {
|
|
357
|
+
super(`OAuth login for "${serverName}" was cancelled.`);
|
|
358
|
+
this.name = "OAuthCancelledError";
|
|
274
359
|
}
|
|
275
|
-
const name = ref.slice("secret://".length).trim();
|
|
276
|
-
return name.length > 0 ? name : null;
|
|
277
360
|
}
|
|
278
|
-
|
|
279
|
-
|
|
361
|
+
|
|
362
|
+
class OAuthLoginInProgressError extends Error {
|
|
363
|
+
code = "oauth_in_progress";
|
|
364
|
+
constructor(serverName, holderPid) {
|
|
365
|
+
super(holderPid ? `OAuth login for "${serverName}" is already in progress (pid ${holderPid}).` : `OAuth login for "${serverName}" is already in progress.`);
|
|
366
|
+
this.name = "OAuthLoginInProgressError";
|
|
367
|
+
}
|
|
280
368
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
369
|
+
function waitForAuthorizationCode(server, serverName, expectedState, options = {}) {
|
|
370
|
+
const timeoutMs = options.timeoutMs ?? OAUTH_LOGIN_TIMEOUT_MS;
|
|
371
|
+
const { signal } = options;
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
let settled = false;
|
|
374
|
+
function settle(fn) {
|
|
375
|
+
if (settled) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
settled = true;
|
|
379
|
+
clearTimeout(timeout);
|
|
380
|
+
signal?.removeEventListener("abort", onAbort);
|
|
381
|
+
fn();
|
|
285
382
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
383
|
+
function onAbort() {
|
|
384
|
+
settle(() => reject(new OAuthCancelledError(serverName)));
|
|
385
|
+
}
|
|
386
|
+
const timeout = setTimeout(() => {
|
|
387
|
+
settle(() => reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`)));
|
|
388
|
+
}, timeoutMs);
|
|
389
|
+
if (signal) {
|
|
390
|
+
if (signal.aborted) {
|
|
391
|
+
settle(() => reject(new OAuthCancelledError(serverName)));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
signal.addEventListener("abort", onAbort);
|
|
395
|
+
}
|
|
396
|
+
server.on("request", (request, response) => {
|
|
397
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
398
|
+
if (settled) {
|
|
399
|
+
response.statusCode = 404;
|
|
400
|
+
response.end();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (url.pathname === "/favicon.ico") {
|
|
404
|
+
response.statusCode = 204;
|
|
405
|
+
response.end();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (url.pathname !== "/callback") {
|
|
409
|
+
response.statusCode = 404;
|
|
410
|
+
response.end();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const code = url.searchParams.get("code");
|
|
414
|
+
const state = url.searchParams.get("state");
|
|
415
|
+
const error = url.searchParams.get("error");
|
|
416
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
417
|
+
if (error) {
|
|
418
|
+
response.statusCode = 400;
|
|
419
|
+
response.end(renderCallbackPage("error", `Authorization failed: ${error}`));
|
|
420
|
+
settle(() => reject(new Error(`OAuth authorization failed: ${error}`)));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (!code) {
|
|
424
|
+
response.statusCode = 400;
|
|
425
|
+
response.end(renderCallbackPage("error", "Missing authorization code."));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (state !== expectedState) {
|
|
429
|
+
response.statusCode = 400;
|
|
430
|
+
response.end(renderCallbackPage("error", "This sign-in link is no longer valid."));
|
|
431
|
+
settle(() => reject(new Error("OAuth state mismatch.")));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
response.end(renderCallbackPage("success"));
|
|
435
|
+
settle(() => resolve(code));
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function bindOAuthReference(config, serverName) {
|
|
440
|
+
const spec = config.servers[serverName];
|
|
441
|
+
if (!spec) {
|
|
442
|
+
throw new Error(`Server "${serverName}" not found.`);
|
|
290
443
|
}
|
|
291
|
-
if (
|
|
292
|
-
throw new Error(
|
|
444
|
+
if (spec.transport !== "http") {
|
|
445
|
+
throw new Error(`OAuth login only supports HTTP servers.`);
|
|
293
446
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
447
|
+
spec.headers = {
|
|
448
|
+
...spec.headers ?? {},
|
|
449
|
+
Authorization: `oauth://${encodeURIComponent(serverName)}`
|
|
297
450
|
};
|
|
298
451
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
spec.headers = headers;
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (spec.transport !== "stdio") {
|
|
310
|
-
throw new Error(`Cannot set env auth on ${spec.transport} upstream.`);
|
|
452
|
+
var OAUTH_LOCK_STALE_MS = 5 * 60000;
|
|
453
|
+
function isProcessAlive(pid) {
|
|
454
|
+
try {
|
|
455
|
+
process.kill(pid, 0);
|
|
456
|
+
return true;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
311
459
|
}
|
|
312
|
-
const env = { ...spec.env ?? {} };
|
|
313
|
-
env[target.key] = secretRef;
|
|
314
|
-
spec.env = env;
|
|
315
460
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
461
|
+
function releaseOAuthLoginLock(lockPath) {
|
|
462
|
+
try {
|
|
463
|
+
fs.unlinkSync(lockPath);
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
466
|
+
function acquireOAuthLoginLock(serverName) {
|
|
467
|
+
const lockPath = getOAuthLockPath(serverName);
|
|
468
|
+
ensureDir(path.dirname(lockPath));
|
|
469
|
+
const tryCreate = () => {
|
|
470
|
+
try {
|
|
471
|
+
fs.writeFileSync(lockPath, `${process.pid}
|
|
472
|
+
`, { flag: "wx" });
|
|
473
|
+
return true;
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (err.code !== "EEXIST") {
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
323
479
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
spec.headers = Object.keys(next2).length > 0 ? next2 : undefined;
|
|
328
|
-
return previous2;
|
|
329
|
-
}
|
|
330
|
-
if (spec.transport !== "stdio") {
|
|
331
|
-
throw new Error(`Cannot remove env auth from ${spec.transport} upstream.`);
|
|
480
|
+
};
|
|
481
|
+
if (tryCreate()) {
|
|
482
|
+
return () => releaseOAuthLoginLock(lockPath);
|
|
332
483
|
}
|
|
333
|
-
|
|
334
|
-
|
|
484
|
+
let holderPid;
|
|
485
|
+
try {
|
|
486
|
+
const raw = fs.readFileSync(lockPath, "utf8").trim();
|
|
487
|
+
const pid = Number(raw);
|
|
488
|
+
const stat = fs.statSync(lockPath);
|
|
489
|
+
const stale = Date.now() - stat.mtimeMs > OAUTH_LOCK_STALE_MS;
|
|
490
|
+
const holderAlive = Number.isFinite(pid) && pid > 0 && isProcessAlive(pid);
|
|
491
|
+
if (!holderAlive || stale) {
|
|
492
|
+
fs.unlinkSync(lockPath);
|
|
493
|
+
if (tryCreate()) {
|
|
494
|
+
return () => releaseOAuthLoginLock(lockPath);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
holderPid = holderAlive ? pid : undefined;
|
|
498
|
+
} catch {
|
|
499
|
+
if (tryCreate()) {
|
|
500
|
+
return () => releaseOAuthLoginLock(lockPath);
|
|
501
|
+
}
|
|
335
502
|
}
|
|
336
|
-
|
|
337
|
-
const next = { ...spec.env };
|
|
338
|
-
delete next[target.key];
|
|
339
|
-
spec.env = Object.keys(next).length > 0 ? next : undefined;
|
|
340
|
-
return previous;
|
|
503
|
+
throw new OAuthLoginInProgressError(serverName, holderPid);
|
|
341
504
|
}
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
value
|
|
348
|
-
}));
|
|
505
|
+
async function runOAuthLogin(serverName, spec, secrets, openUrl, configPath, codeReceiver, options = {}) {
|
|
506
|
+
const { signal, onProgress } = options;
|
|
507
|
+
const timeoutMs = options.timeoutMs ?? OAUTH_LOGIN_TIMEOUT_MS;
|
|
508
|
+
if (signal?.aborted) {
|
|
509
|
+
throw new OAuthCancelledError(serverName);
|
|
349
510
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
511
|
+
const releaseLock = acquireOAuthLoginLock(serverName);
|
|
512
|
+
try {
|
|
513
|
+
const oauthName = oauthSecretName(serverName, "tokens");
|
|
514
|
+
const clientName = oauthSecretName(serverName, "client");
|
|
515
|
+
const backupTokens = secrets.getSecret(oauthName);
|
|
516
|
+
const backupClient = secrets.getSecret(clientName);
|
|
517
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
518
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("discovery");
|
|
519
|
+
const state = crypto.randomUUID();
|
|
520
|
+
let callbackServer;
|
|
521
|
+
let redirectUrl;
|
|
522
|
+
let codePromise;
|
|
523
|
+
if (codeReceiver) {
|
|
524
|
+
redirectUrl = codeReceiver.redirectUrl;
|
|
525
|
+
codePromise = codeReceiver.waitForCode(state, timeoutMs);
|
|
526
|
+
} else {
|
|
527
|
+
callbackServer = http.createServer();
|
|
528
|
+
const port = await listen(callbackServer);
|
|
529
|
+
redirectUrl = `http://127.0.0.1:${port}/callback`;
|
|
530
|
+
codePromise = waitForAuthorizationCode(callbackServer, serverName, state, { timeoutMs, signal });
|
|
531
|
+
}
|
|
532
|
+
codePromise.catch(() => {});
|
|
533
|
+
const wrappedOpenUrl = async (url) => {
|
|
534
|
+
onProgress?.({ phase: "awaiting-browser", authorizationUrl: url, expiresAt: Date.now() + timeoutMs });
|
|
535
|
+
await openUrl(url);
|
|
536
|
+
};
|
|
537
|
+
const provider = new InteractiveOAuthProvider(serverName, secrets, redirectUrl, state, wrappedOpenUrl);
|
|
538
|
+
try {
|
|
539
|
+
onProgress?.({ phase: "discovering" });
|
|
540
|
+
const initial = await auth(provider, {
|
|
541
|
+
serverUrl: spec.url
|
|
542
|
+
});
|
|
543
|
+
if (initial !== "REDIRECT") {
|
|
544
|
+
throw new Error(`Expected OAuth redirect for "${serverName}", got ${initial}.`);
|
|
545
|
+
}
|
|
546
|
+
if (signal?.aborted) {
|
|
547
|
+
throw new OAuthCancelledError(serverName);
|
|
548
|
+
}
|
|
549
|
+
const authorizationCode = await codePromise;
|
|
550
|
+
onProgress?.({ phase: "exchanging" });
|
|
551
|
+
const result = await auth(provider, {
|
|
552
|
+
serverUrl: spec.url,
|
|
553
|
+
authorizationCode
|
|
554
|
+
});
|
|
555
|
+
if (result !== "AUTHORIZED") {
|
|
556
|
+
throw new Error(`OAuth login did not authorize "${serverName}".`);
|
|
557
|
+
}
|
|
558
|
+
onProgress?.({ phase: "syncing" });
|
|
559
|
+
await mutateConfig((config2) => {
|
|
560
|
+
bindOAuthReference(config2, serverName);
|
|
561
|
+
}, configPath);
|
|
562
|
+
const config = loadConfig(configPath);
|
|
563
|
+
const summary = syncAllClients(config, secrets);
|
|
564
|
+
await mutateConfig((freshConfig) => {
|
|
565
|
+
persistSyncState(summary, freshConfig);
|
|
566
|
+
}, configPath);
|
|
567
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
568
|
+
return { serverName, authorized: true };
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (backupTokens)
|
|
571
|
+
secrets.setSecret(oauthName, backupTokens);
|
|
572
|
+
if (backupClient)
|
|
573
|
+
secrets.setSecret(clientName, backupClient);
|
|
574
|
+
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
575
|
+
throw error;
|
|
576
|
+
} finally {
|
|
577
|
+
if (callbackServer) {
|
|
578
|
+
await closeServer(callbackServer);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} finally {
|
|
582
|
+
releaseLock();
|
|
360
583
|
}
|
|
361
|
-
|
|
362
|
-
|
|
584
|
+
}
|
|
585
|
+
async function getOAuthAccessToken(serverName, spec, secrets, options = {}) {
|
|
586
|
+
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
587
|
+
if (!stored) {
|
|
588
|
+
throw new UpstreamError(serverName, "auth_required", `No OAuth tokens stored for "${serverName}".`);
|
|
363
589
|
}
|
|
364
|
-
if (
|
|
365
|
-
|
|
590
|
+
if (options.forceRefresh || tokensAreExpiring(stored)) {
|
|
591
|
+
if (!stored.tokens.refresh_token) {
|
|
592
|
+
if (!options.forceRefresh) {
|
|
593
|
+
return stored.tokens.access_token;
|
|
594
|
+
}
|
|
595
|
+
throw new UpstreamError(serverName, "auth_expired", `No refresh token for "${serverName}". Re-authentication required.`);
|
|
596
|
+
}
|
|
597
|
+
const provider = new McpxOAuthProvider(serverName, secrets);
|
|
598
|
+
const clientInformation = provider.clientInformation();
|
|
599
|
+
if (!clientInformation) {
|
|
600
|
+
throw new UpstreamError(serverName, "auth_expired", `No OAuth client info for "${serverName}". Re-authentication required.`);
|
|
601
|
+
}
|
|
602
|
+
let serverInfo;
|
|
603
|
+
try {
|
|
604
|
+
const cachedDiscovery = provider.discoveryState();
|
|
605
|
+
serverInfo = cachedDiscovery ?? await discoverOAuthServerInfo(spec.url);
|
|
606
|
+
} catch (err) {
|
|
607
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
608
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
609
|
+
throw new UpstreamError(serverName, "unreachable", `OAuth discovery failed: ${message}`);
|
|
610
|
+
}
|
|
611
|
+
throw new UpstreamError(serverName, "upstream_error", `OAuth discovery failed: ${message}`);
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const refreshed = await refreshAuthorization(serverInfo.authorizationServerUrl, {
|
|
615
|
+
metadata: serverInfo.authorizationServerMetadata,
|
|
616
|
+
clientInformation,
|
|
617
|
+
refreshToken: stored.tokens.refresh_token,
|
|
618
|
+
resource: serverInfo.resourceMetadata?.resource ? new URL(serverInfo.resourceMetadata.resource) : undefined
|
|
619
|
+
});
|
|
620
|
+
provider.saveTokens(refreshed);
|
|
621
|
+
return refreshed.access_token;
|
|
622
|
+
} catch (err) {
|
|
623
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
624
|
+
if (err instanceof InvalidGrantError || err instanceof InvalidClientError || /invalid_grant|invalid_client|4\d{2}/i.test(message)) {
|
|
625
|
+
throw new UpstreamError(serverName, "auth_expired", `OAuth refresh failed: ${message}`);
|
|
626
|
+
}
|
|
627
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(message)) {
|
|
628
|
+
throw new UpstreamError(serverName, "unreachable", `OAuth refresh failed: ${message}`);
|
|
629
|
+
}
|
|
630
|
+
throw new UpstreamError(serverName, "upstream_error", `OAuth refresh failed: ${message}`);
|
|
631
|
+
}
|
|
366
632
|
}
|
|
367
|
-
return
|
|
633
|
+
return stored.tokens.access_token;
|
|
368
634
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const current = path.join(nvmDir, "current", "bin");
|
|
377
|
-
return fs.existsSync(current) ? current : null;
|
|
635
|
+
function oauthSecretNames(serverName) {
|
|
636
|
+
return {
|
|
637
|
+
client: oauthSecretName(serverName, "client"),
|
|
638
|
+
tokens: oauthSecretName(serverName, "tokens"),
|
|
639
|
+
verifier: oauthSecretName(serverName, "verifier"),
|
|
640
|
+
discovery: oauthSecretName(serverName, "discovery")
|
|
641
|
+
};
|
|
378
642
|
}
|
|
379
|
-
function
|
|
643
|
+
function clearOAuthCredentials(serverName, secrets) {
|
|
644
|
+
const removed = [];
|
|
380
645
|
const candidates = [
|
|
381
|
-
...
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
646
|
+
...Object.values(oauthSecretNames(serverName)),
|
|
647
|
+
...Object.values({
|
|
648
|
+
client: legacyOAuthSecretName(serverName, "client"),
|
|
649
|
+
tokens: legacyOAuthSecretName(serverName, "tokens"),
|
|
650
|
+
verifier: legacyOAuthSecretName(serverName, "verifier"),
|
|
651
|
+
discovery: legacyOAuthSecretName(serverName, "discovery")
|
|
652
|
+
})
|
|
653
|
+
];
|
|
654
|
+
for (const name of candidates) {
|
|
655
|
+
if (secrets.getSecret(name) !== null) {
|
|
656
|
+
secrets.removeSecret(name);
|
|
657
|
+
removed.push(name);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return removed;
|
|
661
|
+
}
|
|
662
|
+
function getOAuthCredentialStatus(serverName, spec, secrets) {
|
|
663
|
+
const bound = spec.transport === "http" && Object.values(spec.headers ?? {}).some((value) => isOAuthReference(value) && oauthReferenceServerName(value) === serverName);
|
|
664
|
+
const clientInfo = readJsonSecret(secrets, oauthSecretName(serverName, "client"));
|
|
665
|
+
const clientRegistered = clientInfo != null;
|
|
666
|
+
const stored = readJsonSecret(secrets, oauthSecretName(serverName, "tokens"));
|
|
667
|
+
if (!stored) {
|
|
668
|
+
return { bound, signedIn: false, expired: false, hasRefreshToken: false, clientRegistered };
|
|
396
669
|
}
|
|
397
|
-
|
|
670
|
+
const expiresAt = stored.tokens.expires_in ? stored.obtainedAt + stored.tokens.expires_in * 1000 : undefined;
|
|
671
|
+
const expired = expiresAt != null && Date.now() >= expiresAt;
|
|
672
|
+
return {
|
|
673
|
+
bound,
|
|
674
|
+
signedIn: true,
|
|
675
|
+
obtainedAt: stored.obtainedAt,
|
|
676
|
+
expiresAt,
|
|
677
|
+
expired,
|
|
678
|
+
hasRefreshToken: Boolean(stored.tokens.refresh_token),
|
|
679
|
+
clientRegistered
|
|
680
|
+
};
|
|
398
681
|
}
|
|
399
682
|
|
|
400
|
-
// src/core/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
secrets.setSecret(name, JSON.stringify(value));
|
|
425
|
-
}
|
|
426
|
-
function isOAuthReference(value) {
|
|
427
|
-
return value.startsWith("oauth://");
|
|
683
|
+
// src/core/auth-probe.ts
|
|
684
|
+
var JSON_RPC_VERSION = "2.0";
|
|
685
|
+
var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 8000;
|
|
686
|
+
var AUTH_KEYWORDS = [
|
|
687
|
+
"unauthorized",
|
|
688
|
+
"unauthenticated",
|
|
689
|
+
"forbidden",
|
|
690
|
+
"authentication",
|
|
691
|
+
"authorization",
|
|
692
|
+
"api key",
|
|
693
|
+
"api_key",
|
|
694
|
+
"apikey",
|
|
695
|
+
"credentials",
|
|
696
|
+
"access denied",
|
|
697
|
+
"auth required",
|
|
698
|
+
"x-api-key",
|
|
699
|
+
"invalid key",
|
|
700
|
+
"missing header",
|
|
701
|
+
"not authenticated",
|
|
702
|
+
"auth token"
|
|
703
|
+
];
|
|
704
|
+
function isAuthRelatedMessage(message) {
|
|
705
|
+
const lower = message.toLowerCase();
|
|
706
|
+
return AUTH_KEYWORDS.some((kw) => lower.includes(kw));
|
|
428
707
|
}
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
|
|
708
|
+
function resolveHeaders(spec, secrets) {
|
|
709
|
+
const headers = {
|
|
710
|
+
"content-type": "application/json",
|
|
711
|
+
accept: "application/json, text/event-stream"
|
|
712
|
+
};
|
|
713
|
+
for (const [key, value] of Object.entries(spec.headers ?? {})) {
|
|
714
|
+
headers[key] = secrets.resolveMaybeSecret(value);
|
|
432
715
|
}
|
|
433
|
-
return
|
|
716
|
+
return headers;
|
|
434
717
|
}
|
|
435
|
-
function
|
|
436
|
-
if (!
|
|
718
|
+
function isOAuthLikelyChallenge(wwwAuthenticate) {
|
|
719
|
+
if (!wwwAuthenticate) {
|
|
437
720
|
return false;
|
|
438
721
|
}
|
|
439
|
-
const
|
|
440
|
-
return
|
|
722
|
+
const lower = wwwAuthenticate.toLowerCase();
|
|
723
|
+
return lower.includes("bearer") || lower.includes("authorization_uri") || lower.includes("resource_metadata");
|
|
441
724
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this.#serverName = serverName;
|
|
450
|
-
this.#secrets = secrets;
|
|
451
|
-
this.#redirectUrlValue = redirectUrl;
|
|
452
|
-
this.#stateValue = state;
|
|
453
|
-
}
|
|
454
|
-
get redirectUrl() {
|
|
455
|
-
return this.#redirectUrlValue;
|
|
456
|
-
}
|
|
457
|
-
get clientMetadata() {
|
|
458
|
-
if (!this.#redirectUrlValue) {
|
|
459
|
-
return {
|
|
460
|
-
redirect_uris: [],
|
|
461
|
-
token_endpoint_auth_method: "none",
|
|
462
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
463
|
-
response_types: ["code"],
|
|
464
|
-
client_name: "mcpx"
|
|
465
|
-
};
|
|
725
|
+
async function probeHttpAuthRequirement(spec, secrets, timeoutMs = DEFAULT_AUTH_PROBE_TIMEOUT_MS, options = {}) {
|
|
726
|
+
const timeoutController = new AbortController;
|
|
727
|
+
const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
728
|
+
const discoverOAuth = options.discoverOAuth ?? true;
|
|
729
|
+
async function withOAuthSupport(result) {
|
|
730
|
+
if (!discoverOAuth) {
|
|
731
|
+
return result;
|
|
466
732
|
}
|
|
733
|
+
const support = await probeOAuthSupport(spec.url).catch(() => {
|
|
734
|
+
return;
|
|
735
|
+
});
|
|
467
736
|
return {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
response_types: ["code"],
|
|
472
|
-
client_name: "mcpx"
|
|
737
|
+
...result,
|
|
738
|
+
oauthSupport: support?.support,
|
|
739
|
+
oauthLikely: result.oauthLikely || support?.support === "supported"
|
|
473
740
|
};
|
|
474
741
|
}
|
|
475
|
-
|
|
476
|
-
|
|
742
|
+
try {
|
|
743
|
+
const headers = resolveHeaders(spec, secrets);
|
|
744
|
+
const response = await fetch(spec.url, {
|
|
745
|
+
method: "POST",
|
|
746
|
+
headers,
|
|
747
|
+
body: JSON.stringify({
|
|
748
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
749
|
+
id: "mcpx-auth-probe",
|
|
750
|
+
method: "tools/list",
|
|
751
|
+
params: {}
|
|
752
|
+
}),
|
|
753
|
+
signal: timeoutController.signal
|
|
754
|
+
});
|
|
755
|
+
if (response.status === 401 || response.status === 403) {
|
|
756
|
+
const wwwAuthenticate = response.headers.get("www-authenticate");
|
|
757
|
+
return await withOAuthSupport({
|
|
758
|
+
authRequired: true,
|
|
759
|
+
oauthLikely: isOAuthLikelyChallenge(wwwAuthenticate),
|
|
760
|
+
status: response.status,
|
|
761
|
+
wwwAuthenticate: wwwAuthenticate ?? undefined
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (response.status === 200) {
|
|
765
|
+
try {
|
|
766
|
+
const body = await response.json();
|
|
767
|
+
const err = body?.error;
|
|
768
|
+
if (err && typeof err === "object" && !("result" in body)) {
|
|
769
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
770
|
+
if (message && isAuthRelatedMessage(message)) {
|
|
771
|
+
return await withOAuthSupport({
|
|
772
|
+
authRequired: true,
|
|
773
|
+
status: response.status,
|
|
774
|
+
error: `JSON-RPC error: ${message}`
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} catch {}
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
authRequired: false,
|
|
782
|
+
status: response.status
|
|
783
|
+
};
|
|
784
|
+
} catch (error) {
|
|
785
|
+
const isAbort = error.name === "AbortError";
|
|
786
|
+
return {
|
|
787
|
+
authRequired: false,
|
|
788
|
+
error: isAbort ? `Auth probe timed out after ${timeoutMs}ms.` : error.message
|
|
789
|
+
};
|
|
790
|
+
} finally {
|
|
791
|
+
clearTimeout(timeoutHandle);
|
|
477
792
|
}
|
|
478
|
-
|
|
479
|
-
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/core/registry-client.ts
|
|
796
|
+
var REGISTRY_BASE = "https://registry.modelcontextprotocol.io";
|
|
797
|
+
async function fetchRegistryServerDetail(name) {
|
|
798
|
+
const encoded = encodeURIComponent(name);
|
|
799
|
+
const response = await fetch(`${REGISTRY_BASE}/v0.1/servers/${encoded}/versions/latest`, {
|
|
800
|
+
headers: { accept: "application/json" }
|
|
801
|
+
});
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
throw new Error(`Registry API error: ${response.status}`);
|
|
480
804
|
}
|
|
481
|
-
|
|
482
|
-
|
|
805
|
+
const data = await response.json();
|
|
806
|
+
return data.server;
|
|
807
|
+
}
|
|
808
|
+
var PACKAGE_PRIORITY = {
|
|
809
|
+
npm: 1,
|
|
810
|
+
pypi: 2,
|
|
811
|
+
nuget: 3,
|
|
812
|
+
oci: 4,
|
|
813
|
+
mcpb: 5
|
|
814
|
+
};
|
|
815
|
+
var RUNTIME_HINT = {
|
|
816
|
+
npm: "npx",
|
|
817
|
+
pypi: "uvx",
|
|
818
|
+
nuget: "dnx"
|
|
819
|
+
};
|
|
820
|
+
function selectBestPackage(packages = [], remotes = []) {
|
|
821
|
+
const stdioPkgs = packages.filter((p) => p.transport.type === "stdio").sort((a, b) => (PACKAGE_PRIORITY[a.registryType] ?? 99) - (PACKAGE_PRIORITY[b.registryType] ?? 99));
|
|
822
|
+
if (stdioPkgs.length > 0) {
|
|
823
|
+
return { kind: "package", package: stdioPkgs[0] };
|
|
483
824
|
}
|
|
484
|
-
|
|
485
|
-
|
|
825
|
+
const httpRemotes = remotes.filter((r) => r.type === "streamable-http" || r.type === "sse");
|
|
826
|
+
if (httpRemotes.length > 0) {
|
|
827
|
+
return { kind: "remote", remote: httpRemotes[0] };
|
|
486
828
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
tokens,
|
|
490
|
-
obtainedAt: Date.now()
|
|
491
|
-
});
|
|
829
|
+
if (packages.length > 0) {
|
|
830
|
+
return { kind: "package", package: packages[0] };
|
|
492
831
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
832
|
+
if (remotes.length > 0) {
|
|
833
|
+
return { kind: "remote", remote: remotes[0] };
|
|
496
834
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
835
|
+
throw new Error("Server has no packages or remotes");
|
|
836
|
+
}
|
|
837
|
+
function extractRequiredInputs(option) {
|
|
838
|
+
const inputs = [];
|
|
839
|
+
if (option.kind === "package" && option.package?.environmentVariables) {
|
|
840
|
+
for (const env of option.package.environmentVariables) {
|
|
841
|
+
if (env.isRequired && !env.default) {
|
|
842
|
+
inputs.push({
|
|
843
|
+
name: env.name,
|
|
844
|
+
description: env.description,
|
|
845
|
+
isSecret: env.isSecret ?? false,
|
|
846
|
+
kind: "env"
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
for (const arg of option.package.packageArguments ?? []) {
|
|
851
|
+
if (arg.isRequired && !arg.value && !arg.default) {
|
|
852
|
+
inputs.push({
|
|
853
|
+
name: arg.name ?? arg.valueHint ?? "arg",
|
|
854
|
+
description: arg.description,
|
|
855
|
+
isSecret: false,
|
|
856
|
+
kind: "arg"
|
|
857
|
+
});
|
|
858
|
+
}
|
|
501
859
|
}
|
|
502
|
-
return verifier;
|
|
503
|
-
}
|
|
504
|
-
saveDiscoveryState(state) {
|
|
505
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"), state);
|
|
506
|
-
}
|
|
507
|
-
discoveryState() {
|
|
508
|
-
return readJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "discovery"));
|
|
509
860
|
}
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
861
|
+
if (option.kind === "remote" && option.remote?.headers) {
|
|
862
|
+
for (const header of option.remote.headers) {
|
|
863
|
+
if (header.isRequired && !header.default) {
|
|
864
|
+
inputs.push({
|
|
865
|
+
name: header.name,
|
|
866
|
+
description: header.description,
|
|
867
|
+
isSecret: header.isSecret ?? false,
|
|
868
|
+
kind: "header"
|
|
869
|
+
});
|
|
870
|
+
}
|
|
514
871
|
}
|
|
515
872
|
}
|
|
873
|
+
return inputs;
|
|
516
874
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
this.#cachedClientInfo ??= super.clientInformation();
|
|
531
|
-
return this.#cachedClientInfo;
|
|
532
|
-
}
|
|
533
|
-
saveClientInformation(clientInformation) {
|
|
534
|
-
super.saveClientInformation(clientInformation);
|
|
535
|
-
this.#cachedClientInfo = clientInformation;
|
|
875
|
+
function mapRegistryToSpec(_name, option, resolvedValues) {
|
|
876
|
+
if (option.kind === "remote") {
|
|
877
|
+
const headers = {};
|
|
878
|
+
for (const header of option.remote?.headers ?? []) {
|
|
879
|
+
const value = resolvedValues[header.name] ?? header.default;
|
|
880
|
+
if (value)
|
|
881
|
+
headers[header.name] = value;
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
transport: "http",
|
|
885
|
+
url: option.remote.url,
|
|
886
|
+
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
887
|
+
};
|
|
536
888
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
889
|
+
const pkg = option.package;
|
|
890
|
+
const runtime = pkg.runtimeHint ?? RUNTIME_HINT[pkg.registryType] ?? "npx";
|
|
891
|
+
const args = [];
|
|
892
|
+
const identifier = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
|
|
893
|
+
args.push(identifier);
|
|
894
|
+
for (const arg of pkg.packageArguments ?? []) {
|
|
895
|
+
const value = arg.value ?? resolvedValues[arg.name ?? arg.valueHint ?? ""] ?? arg.default;
|
|
896
|
+
if (!value)
|
|
897
|
+
continue;
|
|
898
|
+
if (arg.type === "named" && arg.name) {
|
|
899
|
+
args.push(arg.name, value);
|
|
900
|
+
} else {
|
|
901
|
+
args.push(value);
|
|
540
902
|
}
|
|
541
|
-
return super.codeVerifier();
|
|
542
903
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
904
|
+
const env = {};
|
|
905
|
+
for (const envVar of pkg.environmentVariables ?? []) {
|
|
906
|
+
const value = resolvedValues[envVar.name] ?? envVar.default;
|
|
907
|
+
if (value)
|
|
908
|
+
env[envVar.name] = value;
|
|
546
909
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
910
|
+
return {
|
|
911
|
+
transport: "stdio",
|
|
912
|
+
command: runtime,
|
|
913
|
+
args,
|
|
914
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/core/server-auth.ts
|
|
919
|
+
function normalizeNamePart(value) {
|
|
920
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
921
|
+
}
|
|
922
|
+
function requireNonEmpty(value, label) {
|
|
923
|
+
const normalized = value?.trim();
|
|
924
|
+
if (!normalized) {
|
|
925
|
+
throw new Error(`${label} must be non-empty.`);
|
|
552
926
|
}
|
|
927
|
+
return normalized;
|
|
553
928
|
}
|
|
554
|
-
function
|
|
555
|
-
return
|
|
556
|
-
server.once("error", reject);
|
|
557
|
-
server.listen(0, "127.0.0.1", () => {
|
|
558
|
-
const address = server.address();
|
|
559
|
-
if (!address || typeof address === "string") {
|
|
560
|
-
reject(new Error("Failed to start OAuth callback server."));
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
resolve(address.port);
|
|
564
|
-
});
|
|
565
|
-
});
|
|
929
|
+
function toSecretRef(secretName) {
|
|
930
|
+
return `secret://${secretName}`;
|
|
566
931
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
932
|
+
function secretRefName(ref) {
|
|
933
|
+
if (!ref.startsWith("secret://")) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const name = ref.slice("secret://".length).trim();
|
|
937
|
+
return name.length > 0 ? name : null;
|
|
571
938
|
}
|
|
572
|
-
function
|
|
573
|
-
return
|
|
574
|
-
const timeout = setTimeout(() => {
|
|
575
|
-
reject(new Error(`OAuth login timed out after ${timeoutMs}ms.`));
|
|
576
|
-
}, timeoutMs);
|
|
577
|
-
server.on("request", (request, response) => {
|
|
578
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
579
|
-
const code = url.searchParams.get("code");
|
|
580
|
-
const state = url.searchParams.get("state");
|
|
581
|
-
const error = url.searchParams.get("error");
|
|
582
|
-
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
583
|
-
if (error) {
|
|
584
|
-
response.statusCode = 400;
|
|
585
|
-
response.end("<html><body><h1>mcpx OAuth failed</h1><p>You can close this window.</p></body></html>");
|
|
586
|
-
clearTimeout(timeout);
|
|
587
|
-
reject(new Error(`OAuth authorization failed: ${error}`));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
if (!code) {
|
|
591
|
-
response.statusCode = 400;
|
|
592
|
-
response.end("<html><body><h1>Missing OAuth code</h1><p>You can close this window.</p></body></html>");
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (state !== expectedState) {
|
|
596
|
-
response.statusCode = 400;
|
|
597
|
-
response.end("<html><body><h1>OAuth state mismatch</h1><p>You can close this window.</p></body></html>");
|
|
598
|
-
clearTimeout(timeout);
|
|
599
|
-
reject(new Error("OAuth state mismatch."));
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
response.end("<html><body><h1>mcpx OAuth complete</h1><p>You can close this window.</p></body></html>");
|
|
603
|
-
clearTimeout(timeout);
|
|
604
|
-
resolve(code);
|
|
605
|
-
});
|
|
606
|
-
});
|
|
939
|
+
function defaultAuthSecretName(serverName, target) {
|
|
940
|
+
return `auth_${normalizeNamePart(serverName)}_${target.kind}_${normalizeNamePart(target.key)}`;
|
|
607
941
|
}
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
942
|
+
function resolveAuthTarget(spec, header, env) {
|
|
943
|
+
if (spec.transport === "http") {
|
|
944
|
+
if (env) {
|
|
945
|
+
throw new Error("--env is only valid for stdio upstreams.");
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
kind: "header",
|
|
949
|
+
key: requireNonEmpty(header ?? "Authorization", "Header name")
|
|
950
|
+
};
|
|
612
951
|
}
|
|
613
|
-
if (
|
|
614
|
-
throw new Error(
|
|
952
|
+
if (header) {
|
|
953
|
+
throw new Error("--header is only valid for HTTP upstreams.");
|
|
615
954
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
955
|
+
return {
|
|
956
|
+
kind: "env",
|
|
957
|
+
key: requireNonEmpty(env, "Env var name")
|
|
619
958
|
};
|
|
620
959
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
let redirectUrl;
|
|
631
|
-
let codePromise;
|
|
632
|
-
if (codeReceiver) {
|
|
633
|
-
redirectUrl = codeReceiver.redirectUrl;
|
|
634
|
-
codePromise = codeReceiver.waitForCode(state);
|
|
635
|
-
} else {
|
|
636
|
-
callbackServer = http.createServer();
|
|
637
|
-
const port = await listen(callbackServer);
|
|
638
|
-
redirectUrl = `http://127.0.0.1:${port}/callback`;
|
|
639
|
-
codePromise = waitForAuthorizationCode(callbackServer, state);
|
|
960
|
+
function applyAuthReference(spec, target, secretRef) {
|
|
961
|
+
if (target.kind === "header") {
|
|
962
|
+
if (spec.transport !== "http") {
|
|
963
|
+
throw new Error(`Cannot set header auth on ${spec.transport} upstream.`);
|
|
964
|
+
}
|
|
965
|
+
const headers = { ...spec.headers ?? {} };
|
|
966
|
+
headers[target.key] = secretRef;
|
|
967
|
+
spec.headers = headers;
|
|
968
|
+
return;
|
|
640
969
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
970
|
+
if (spec.transport !== "stdio") {
|
|
971
|
+
throw new Error(`Cannot set env auth on ${spec.transport} upstream.`);
|
|
972
|
+
}
|
|
973
|
+
const env = { ...spec.env ?? {} };
|
|
974
|
+
env[target.key] = secretRef;
|
|
975
|
+
spec.env = env;
|
|
976
|
+
}
|
|
977
|
+
function removeAuthReference(spec, target) {
|
|
978
|
+
if (target.kind === "header") {
|
|
979
|
+
if (spec.transport !== "http") {
|
|
980
|
+
throw new Error(`Cannot remove header auth from ${spec.transport} upstream.`);
|
|
649
981
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
serverUrl: spec.url,
|
|
653
|
-
authorizationCode
|
|
654
|
-
});
|
|
655
|
-
if (result !== "AUTHORIZED") {
|
|
656
|
-
throw new Error(`OAuth login did not authorize "${serverName}".`);
|
|
657
|
-
}
|
|
658
|
-
const config = loadConfig(configPath);
|
|
659
|
-
bindOAuthReference(config, serverName);
|
|
660
|
-
saveConfig(config, configPath);
|
|
661
|
-
const summary = syncAllClients(config, secrets);
|
|
662
|
-
persistSyncState(summary, config);
|
|
663
|
-
saveConfig(config, configPath);
|
|
664
|
-
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
665
|
-
return { serverName, authorized: true };
|
|
666
|
-
} catch (error) {
|
|
667
|
-
if (backupTokens)
|
|
668
|
-
secrets.setSecret(oauthName, backupTokens);
|
|
669
|
-
if (backupClient)
|
|
670
|
-
secrets.setSecret(clientName, backupClient);
|
|
671
|
-
new McpxOAuthProvider(serverName, secrets).invalidateCredentials("verifier");
|
|
672
|
-
throw error;
|
|
673
|
-
} finally {
|
|
674
|
-
if (callbackServer) {
|
|
675
|
-
await closeServer(callbackServer);
|
|
982
|
+
if (!spec.headers || !(target.key in spec.headers)) {
|
|
983
|
+
return null;
|
|
676
984
|
}
|
|
985
|
+
const previous2 = spec.headers[target.key] ?? null;
|
|
986
|
+
const next2 = { ...spec.headers };
|
|
987
|
+
delete next2[target.key];
|
|
988
|
+
spec.headers = Object.keys(next2).length > 0 ? next2 : undefined;
|
|
989
|
+
return previous2;
|
|
990
|
+
}
|
|
991
|
+
if (spec.transport !== "stdio") {
|
|
992
|
+
throw new Error(`Cannot remove env auth from ${spec.transport} upstream.`);
|
|
993
|
+
}
|
|
994
|
+
if (!spec.env || !(target.key in spec.env)) {
|
|
995
|
+
return null;
|
|
677
996
|
}
|
|
997
|
+
const previous = spec.env[target.key] ?? null;
|
|
998
|
+
const next = { ...spec.env };
|
|
999
|
+
delete next[target.key];
|
|
1000
|
+
spec.env = Object.keys(next).length > 0 ? next : undefined;
|
|
1001
|
+
return previous;
|
|
678
1002
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1003
|
+
function listAuthBindings(spec) {
|
|
1004
|
+
if (spec.transport === "http") {
|
|
1005
|
+
return Object.entries(spec.headers ?? {}).map(([key, value]) => ({
|
|
1006
|
+
kind: "header",
|
|
1007
|
+
key,
|
|
1008
|
+
value
|
|
1009
|
+
}));
|
|
683
1010
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1011
|
+
return Object.entries(spec.env ?? {}).map(([key, value]) => ({
|
|
1012
|
+
kind: "env",
|
|
1013
|
+
key,
|
|
1014
|
+
value
|
|
1015
|
+
}));
|
|
1016
|
+
}
|
|
1017
|
+
var TOKEN68 = /^[A-Za-z0-9\-._~+/]+=*$/;
|
|
1018
|
+
function maybePrefixBearer(target, value, raw = false) {
|
|
1019
|
+
if (raw || target.kind !== "header" || target.key.toLowerCase() !== "authorization") {
|
|
1020
|
+
return value;
|
|
1021
|
+
}
|
|
1022
|
+
if (/^\S+\s+\S+/.test(value)) {
|
|
1023
|
+
return value;
|
|
1024
|
+
}
|
|
1025
|
+
if (TOKEN68.test(value)) {
|
|
1026
|
+
return `Bearer ${value}`;
|
|
1027
|
+
}
|
|
1028
|
+
return value;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/core/spawn-env.ts
|
|
1032
|
+
import os from "node:os";
|
|
1033
|
+
import path2 from "node:path";
|
|
1034
|
+
import fs2 from "node:fs";
|
|
1035
|
+
function existingNvmCurrentBin() {
|
|
1036
|
+
const nvmDir = process.env.NVM_DIR ?? path2.join(os.homedir(), ".nvm");
|
|
1037
|
+
const current = path2.join(nvmDir, "current", "bin");
|
|
1038
|
+
return fs2.existsSync(current) ? current : null;
|
|
1039
|
+
}
|
|
1040
|
+
function buildEnrichedPath(current = process.env.PATH ?? "") {
|
|
1041
|
+
const candidates = [
|
|
1042
|
+
...current.split(":"),
|
|
1043
|
+
"/opt/homebrew/bin",
|
|
1044
|
+
"/usr/local/bin",
|
|
1045
|
+
path2.join(os.homedir(), ".bun", "bin"),
|
|
1046
|
+
path2.join(os.homedir(), ".local", "bin"),
|
|
1047
|
+
existingNvmCurrentBin()
|
|
1048
|
+
].filter((entry) => Boolean(entry));
|
|
1049
|
+
const seen = new Set;
|
|
1050
|
+
const entries = [];
|
|
1051
|
+
for (const candidate of candidates) {
|
|
1052
|
+
if (!candidate || seen.has(candidate)) {
|
|
1053
|
+
continue;
|
|
725
1054
|
}
|
|
1055
|
+
seen.add(candidate);
|
|
1056
|
+
entries.push(candidate);
|
|
726
1057
|
}
|
|
727
|
-
return
|
|
1058
|
+
return entries.join(":");
|
|
728
1059
|
}
|
|
729
1060
|
|
|
730
1061
|
// src/core/update.ts
|
|
731
|
-
import
|
|
732
|
-
import
|
|
1062
|
+
import fs3 from "node:fs";
|
|
1063
|
+
import path3 from "node:path";
|
|
733
1064
|
import { execFileSync } from "node:child_process";
|
|
734
1065
|
function compareVersions(a, b) {
|
|
735
1066
|
const partsA = a.split(".").map(Number);
|
|
@@ -768,17 +1099,17 @@ async function checkForUpdates() {
|
|
|
768
1099
|
}
|
|
769
1100
|
function getStagedUpdate() {
|
|
770
1101
|
const stagedPath = getStagedVersionPath();
|
|
771
|
-
if (!
|
|
1102
|
+
if (!fs3.existsSync(stagedPath)) {
|
|
772
1103
|
return null;
|
|
773
1104
|
}
|
|
774
1105
|
try {
|
|
775
|
-
const raw =
|
|
1106
|
+
const raw = fs3.readFileSync(stagedPath, "utf8");
|
|
776
1107
|
const info = JSON.parse(raw);
|
|
777
1108
|
if (!info.version || !info.cliPath) {
|
|
778
1109
|
return null;
|
|
779
1110
|
}
|
|
780
|
-
const absoluteCliPath =
|
|
781
|
-
if (!
|
|
1111
|
+
const absoluteCliPath = path3.isAbsolute(info.cliPath) ? info.cliPath : path3.join(getUpdatesDir(), info.cliPath);
|
|
1112
|
+
if (!fs3.existsSync(absoluteCliPath)) {
|
|
782
1113
|
return null;
|
|
783
1114
|
}
|
|
784
1115
|
return {
|
|
@@ -794,18 +1125,18 @@ function stageUpdate(version, cliPath) {
|
|
|
794
1125
|
ensureDir(updatesDir);
|
|
795
1126
|
const stagedInfo = {
|
|
796
1127
|
version,
|
|
797
|
-
cliPath:
|
|
1128
|
+
cliPath: path3.isAbsolute(cliPath) ? path3.relative(updatesDir, cliPath) : cliPath,
|
|
798
1129
|
stagedAt: new Date().toISOString()
|
|
799
1130
|
};
|
|
800
|
-
|
|
1131
|
+
fs3.writeFileSync(getStagedVersionPath(), JSON.stringify(stagedInfo, null, 2), "utf8");
|
|
801
1132
|
}
|
|
802
1133
|
function shouldUseStagedCli(stagedVersion, currentVersion) {
|
|
803
1134
|
return compareVersions(stagedVersion, currentVersion) > 0;
|
|
804
1135
|
}
|
|
805
1136
|
function clearStagedUpdate() {
|
|
806
1137
|
const stagedPath = getStagedVersionPath();
|
|
807
|
-
if (
|
|
808
|
-
|
|
1138
|
+
if (fs3.existsSync(stagedPath)) {
|
|
1139
|
+
fs3.unlinkSync(stagedPath);
|
|
809
1140
|
}
|
|
810
1141
|
}
|
|
811
1142
|
function getStagedCliPath() {
|
|
@@ -814,31 +1145,33 @@ function getStagedCliPath() {
|
|
|
814
1145
|
}
|
|
815
1146
|
function removeOldVersions(keepCount = 2) {
|
|
816
1147
|
const updatesDir = getUpdatesDir();
|
|
817
|
-
if (!
|
|
1148
|
+
if (!fs3.existsSync(updatesDir)) {
|
|
818
1149
|
return;
|
|
819
1150
|
}
|
|
820
|
-
const entries =
|
|
1151
|
+
const entries = fs3.readdirSync(updatesDir, { withFileTypes: true });
|
|
821
1152
|
const versionDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith("v")).map((entry) => entry.name).sort((a, b) => compareVersions(b.slice(1), a.slice(1)));
|
|
822
1153
|
if (versionDirs.length <= keepCount) {
|
|
823
1154
|
return;
|
|
824
1155
|
}
|
|
825
1156
|
for (const oldVersion of versionDirs.slice(keepCount)) {
|
|
826
|
-
const oldPath =
|
|
1157
|
+
const oldPath = path3.join(updatesDir, oldVersion);
|
|
827
1158
|
try {
|
|
828
|
-
|
|
1159
|
+
fs3.rmSync(oldPath, { recursive: true, force: true });
|
|
829
1160
|
} catch {}
|
|
830
1161
|
}
|
|
831
1162
|
}
|
|
832
1163
|
|
|
833
1164
|
// src/core/update-manager.ts
|
|
834
|
-
import
|
|
835
|
-
import
|
|
836
|
-
import {
|
|
1165
|
+
import fs4 from "node:fs";
|
|
1166
|
+
import path4 from "node:path";
|
|
1167
|
+
import { execFile, spawn } from "node:child_process";
|
|
1168
|
+
import { promisify } from "node:util";
|
|
1169
|
+
var execFileAsync = promisify(execFile);
|
|
837
1170
|
function acquireLock() {
|
|
838
1171
|
const lockPath = getUpdateLockPath();
|
|
839
1172
|
try {
|
|
840
|
-
ensureDir(
|
|
841
|
-
|
|
1173
|
+
ensureDir(path4.dirname(lockPath));
|
|
1174
|
+
fs4.writeFileSync(lockPath, `${process.pid}
|
|
842
1175
|
`, { flag: "wx" });
|
|
843
1176
|
return true;
|
|
844
1177
|
} catch {
|
|
@@ -848,25 +1181,25 @@ function acquireLock() {
|
|
|
848
1181
|
function releaseLock() {
|
|
849
1182
|
const lockPath = getUpdateLockPath();
|
|
850
1183
|
try {
|
|
851
|
-
|
|
1184
|
+
fs4.unlinkSync(lockPath);
|
|
852
1185
|
} catch {}
|
|
853
1186
|
}
|
|
854
1187
|
function isUpdateInProgress() {
|
|
855
1188
|
const lockPath = getUpdateLockPath();
|
|
856
|
-
if (!
|
|
1189
|
+
if (!fs4.existsSync(lockPath)) {
|
|
857
1190
|
return false;
|
|
858
1191
|
}
|
|
859
1192
|
try {
|
|
860
|
-
const raw =
|
|
1193
|
+
const raw = fs4.readFileSync(lockPath, "utf8").trim();
|
|
861
1194
|
const pid = Number(raw);
|
|
862
1195
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
863
|
-
|
|
1196
|
+
fs4.unlinkSync(lockPath);
|
|
864
1197
|
return false;
|
|
865
1198
|
}
|
|
866
1199
|
process.kill(pid, 0);
|
|
867
1200
|
return true;
|
|
868
1201
|
} catch {
|
|
869
|
-
|
|
1202
|
+
fs4.unlinkSync(lockPath);
|
|
870
1203
|
return false;
|
|
871
1204
|
}
|
|
872
1205
|
}
|
|
@@ -876,9 +1209,12 @@ async function downloadAndStageUpdate() {
|
|
|
876
1209
|
return { success: false, error: "No update available" };
|
|
877
1210
|
}
|
|
878
1211
|
const targetVersion = updateStatus.latestVersion;
|
|
1212
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(targetVersion)) {
|
|
1213
|
+
return { success: false, error: "Update returned an invalid version" };
|
|
1214
|
+
}
|
|
879
1215
|
const updatesDir = getUpdatesDir();
|
|
880
|
-
const versionDir =
|
|
881
|
-
if (
|
|
1216
|
+
const versionDir = path4.join(updatesDir, `v${targetVersion}`);
|
|
1217
|
+
if (fs4.existsSync(versionDir)) {
|
|
882
1218
|
const existingStaged = getStagedUpdate();
|
|
883
1219
|
if (existingStaged?.version === targetVersion) {
|
|
884
1220
|
return { success: false, error: `Version ${targetVersion} already staged` };
|
|
@@ -886,31 +1222,19 @@ async function downloadAndStageUpdate() {
|
|
|
886
1222
|
}
|
|
887
1223
|
try {
|
|
888
1224
|
ensureDir(versionDir);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
});
|
|
893
|
-
const tarballPath = path3.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
|
|
894
|
-
if (!fs3.existsSync(tarballPath)) {
|
|
1225
|
+
await execFileAsync("npm", ["pack", `@kwonye/mcpx@${targetVersion}`, "--json"], { cwd: versionDir });
|
|
1226
|
+
const tarballPath = path4.join(versionDir, `kwonye-mcpx-${targetVersion}.tgz`);
|
|
1227
|
+
if (!fs4.existsSync(tarballPath)) {
|
|
895
1228
|
throw new Error("Downloaded tarball not found");
|
|
896
1229
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
execSync("npm install --production --ignore-scripts", {
|
|
902
|
-
cwd: versionDir,
|
|
903
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
904
|
-
});
|
|
905
|
-
const cliPath = path3.join(versionDir, "dist", "cli.js");
|
|
906
|
-
if (!fs3.existsSync(cliPath)) {
|
|
1230
|
+
await execFileAsync("tar", ["-xzf", tarballPath, "--strip-components=1"], { cwd: versionDir });
|
|
1231
|
+
await execFileAsync("npm", ["install", "--production", "--ignore-scripts"], { cwd: versionDir });
|
|
1232
|
+
const cliPath = path4.join(versionDir, "dist", "cli.js");
|
|
1233
|
+
if (!fs4.existsSync(cliPath)) {
|
|
907
1234
|
throw new Error("CLI entry point not found in downloaded package");
|
|
908
1235
|
}
|
|
909
1236
|
try {
|
|
910
|
-
|
|
911
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
912
|
-
timeout: 1e4
|
|
913
|
-
});
|
|
1237
|
+
await execFileAsync(process.execPath, [cliPath, "--version"], { timeout: 1e4 });
|
|
914
1238
|
} catch (smokeTestError) {
|
|
915
1239
|
throw new Error(`Downloaded CLI failed smoke test (--version): ${smokeTestError instanceof Error ? smokeTestError.message : String(smokeTestError)}`);
|
|
916
1240
|
}
|
|
@@ -919,7 +1243,7 @@ async function downloadAndStageUpdate() {
|
|
|
919
1243
|
return { success: true, version: targetVersion };
|
|
920
1244
|
} catch (error) {
|
|
921
1245
|
try {
|
|
922
|
-
|
|
1246
|
+
fs4.rmSync(versionDir, { recursive: true, force: true });
|
|
923
1247
|
} catch {}
|
|
924
1248
|
return {
|
|
925
1249
|
success: false,
|
|
@@ -950,9 +1274,10 @@ function startBackgroundUpdateCheck() {
|
|
|
950
1274
|
}
|
|
951
1275
|
});
|
|
952
1276
|
child.unref();
|
|
953
|
-
setTimeout(() => {
|
|
1277
|
+
const releaseTimer = setTimeout(() => {
|
|
954
1278
|
releaseLock();
|
|
955
1279
|
}, 60000);
|
|
1280
|
+
releaseTimer.unref?.();
|
|
956
1281
|
}
|
|
957
1282
|
async function runBackgroundUpdate() {
|
|
958
1283
|
try {
|
|
@@ -1007,13 +1332,13 @@ function performRollback() {
|
|
|
1007
1332
|
}
|
|
1008
1333
|
|
|
1009
1334
|
// src/core/marketplace-updater.ts
|
|
1010
|
-
import
|
|
1335
|
+
import fs5 from "node:fs";
|
|
1011
1336
|
var MARKETPLACE_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
1012
1337
|
var INITIAL_DELAY_MS = 30000;
|
|
1013
1338
|
var LOCK_STALE_MS = 30 * 60 * 1000;
|
|
1014
1339
|
function readState() {
|
|
1015
1340
|
try {
|
|
1016
|
-
return JSON.parse(
|
|
1341
|
+
return JSON.parse(fs5.readFileSync(getMarketplaceUpdateStatePath(), "utf8"));
|
|
1017
1342
|
} catch {
|
|
1018
1343
|
return {};
|
|
1019
1344
|
}
|
|
@@ -1021,7 +1346,7 @@ function readState() {
|
|
|
1021
1346
|
function writeState(state) {
|
|
1022
1347
|
const target = getMarketplaceUpdateStatePath();
|
|
1023
1348
|
ensureParentDir(target);
|
|
1024
|
-
|
|
1349
|
+
fs5.writeFileSync(target, JSON.stringify(state, null, 2), { mode: 384 });
|
|
1025
1350
|
}
|
|
1026
1351
|
function due(now = Date.now()) {
|
|
1027
1352
|
const last = readState().lastRunAt;
|
|
@@ -1031,13 +1356,13 @@ function acquireLock2() {
|
|
|
1031
1356
|
const target = getMarketplaceUpdateLockPath();
|
|
1032
1357
|
ensureParentDir(target);
|
|
1033
1358
|
try {
|
|
1034
|
-
|
|
1359
|
+
fs5.writeFileSync(target, `${process.pid}
|
|
1035
1360
|
`, { flag: "wx", mode: 384 });
|
|
1036
1361
|
return true;
|
|
1037
1362
|
} catch {
|
|
1038
1363
|
try {
|
|
1039
|
-
if (Date.now() -
|
|
1040
|
-
|
|
1364
|
+
if (Date.now() - fs5.statSync(target).mtimeMs > LOCK_STALE_MS) {
|
|
1365
|
+
fs5.unlinkSync(target);
|
|
1041
1366
|
return acquireLock2();
|
|
1042
1367
|
}
|
|
1043
1368
|
} catch {
|
|
@@ -1048,7 +1373,7 @@ function acquireLock2() {
|
|
|
1048
1373
|
}
|
|
1049
1374
|
function releaseLock2() {
|
|
1050
1375
|
try {
|
|
1051
|
-
|
|
1376
|
+
fs5.unlinkSync(getMarketplaceUpdateLockPath());
|
|
1052
1377
|
} catch {}
|
|
1053
1378
|
}
|
|
1054
1379
|
async function runMarketplaceAutoUpdate(options) {
|
|
@@ -1088,7 +1413,7 @@ function startMarketplaceAutoUpdater() {
|
|
|
1088
1413
|
}
|
|
1089
1414
|
|
|
1090
1415
|
// src/core/daemon.ts
|
|
1091
|
-
import
|
|
1416
|
+
import fs6 from "node:fs";
|
|
1092
1417
|
import net from "node:net";
|
|
1093
1418
|
import { spawn as spawn2, execFileSync as execFileSync2 } from "node:child_process";
|
|
1094
1419
|
|
|
@@ -1258,6 +1583,29 @@ function parseNamespacedUri(uri) {
|
|
|
1258
1583
|
async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
|
|
1259
1584
|
return callUpstreamOnce(upstream, method, params, secrets, runtime, passthroughAuthorizationHeader, false);
|
|
1260
1585
|
}
|
|
1586
|
+
var ERROR_CODE_PRIORITY = [
|
|
1587
|
+
"auth_expired",
|
|
1588
|
+
"auth_required",
|
|
1589
|
+
"secret_missing",
|
|
1590
|
+
"unreachable",
|
|
1591
|
+
"timeout",
|
|
1592
|
+
"upstream_error"
|
|
1593
|
+
];
|
|
1594
|
+
function pickPrimaryErrorCode(errors) {
|
|
1595
|
+
let best;
|
|
1596
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
1597
|
+
for (const { code } of errors) {
|
|
1598
|
+
if (!code)
|
|
1599
|
+
continue;
|
|
1600
|
+
const rank = ERROR_CODE_PRIORITY.indexOf(code);
|
|
1601
|
+
const effectiveRank = rank === -1 ? ERROR_CODE_PRIORITY.length : rank;
|
|
1602
|
+
if (effectiveRank < bestRank) {
|
|
1603
|
+
bestRank = effectiveRank;
|
|
1604
|
+
best = code;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return best;
|
|
1608
|
+
}
|
|
1261
1609
|
async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
1262
1610
|
if (!runtime.tokenCache) {
|
|
1263
1611
|
runtime.tokenCache = new Map;
|
|
@@ -1265,7 +1613,11 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1265
1613
|
const results = {};
|
|
1266
1614
|
const upstreams = listUpstreams(config);
|
|
1267
1615
|
for (const upstream of upstreams) {
|
|
1268
|
-
|
|
1616
|
+
let recordError = function(label, error) {
|
|
1617
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1618
|
+
errors.push({ label, message, code: error instanceof UpstreamError ? error.code : undefined });
|
|
1619
|
+
};
|
|
1620
|
+
const fingerprint = specFingerprint(upstream.spec, secrets);
|
|
1269
1621
|
const cached = runtime.tokenCache.get(upstream.name);
|
|
1270
1622
|
if (cached?.fingerprint === fingerprint) {
|
|
1271
1623
|
const runtimeErr2 = runtime.upstreamErrors?.get(upstream.name);
|
|
@@ -1282,7 +1634,7 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1282
1634
|
toolsCount = Math.ceil(JSON.stringify(toolsResult.tools).length / 4);
|
|
1283
1635
|
}
|
|
1284
1636
|
} catch (error) {
|
|
1285
|
-
|
|
1637
|
+
recordError("tools/list", error);
|
|
1286
1638
|
}
|
|
1287
1639
|
try {
|
|
1288
1640
|
const resourcesResult = await callUpstream(upstream, "resources/list", {}, "token-resources", secrets, runtime);
|
|
@@ -1290,7 +1642,7 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1290
1642
|
resourcesCount = Math.ceil(JSON.stringify(resourcesResult.resources).length / 4);
|
|
1291
1643
|
}
|
|
1292
1644
|
} catch (error) {
|
|
1293
|
-
|
|
1645
|
+
recordError("resources/list", error);
|
|
1294
1646
|
}
|
|
1295
1647
|
try {
|
|
1296
1648
|
const promptsResult = await callUpstream(upstream, "prompts/list", {}, "token-prompts", secrets, runtime);
|
|
@@ -1298,7 +1650,7 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1298
1650
|
promptsCount = Math.ceil(JSON.stringify(promptsResult.prompts).length / 4);
|
|
1299
1651
|
}
|
|
1300
1652
|
} catch (error) {
|
|
1301
|
-
|
|
1653
|
+
recordError("prompts/list", error);
|
|
1302
1654
|
}
|
|
1303
1655
|
const total = toolsCount + resourcesCount + promptsCount;
|
|
1304
1656
|
const countObj = {
|
|
@@ -1306,7 +1658,8 @@ async function getUpstreamTokenCounts(config, secrets, runtime) {
|
|
|
1306
1658
|
resources: resourcesCount,
|
|
1307
1659
|
prompts: promptsCount,
|
|
1308
1660
|
total,
|
|
1309
|
-
error: errors.length > 0 ? errors.join("; ") : undefined
|
|
1661
|
+
error: errors.length > 0 ? errors.map((e) => `${e.label}: ${e.message}`).join("; ") : undefined,
|
|
1662
|
+
errorCode: pickPrimaryErrorCode(errors)
|
|
1310
1663
|
};
|
|
1311
1664
|
if (errors.length === 0) {
|
|
1312
1665
|
runtime.tokenCache.set(upstream.name, { fingerprint, count: countObj });
|
|
@@ -1340,7 +1693,20 @@ function specFingerprint(spec, secrets) {
|
|
|
1340
1693
|
if (!obj)
|
|
1341
1694
|
return;
|
|
1342
1695
|
for (const [key, value] of Object.entries(obj)) {
|
|
1343
|
-
if (
|
|
1696
|
+
if (isOAuthReference(value)) {
|
|
1697
|
+
try {
|
|
1698
|
+
const serverName = oauthReferenceServerName(value);
|
|
1699
|
+
let storedTokens = secrets.getSecret(oauthSecretNames(serverName).tokens);
|
|
1700
|
+
if (!storedTokens) {
|
|
1701
|
+
const legacy = `oauth_${serverName.toLowerCase().replace(/[^a-z0-9._-]/g, "_")}_tokens`;
|
|
1702
|
+
storedTokens = secrets.getSecret(legacy);
|
|
1703
|
+
}
|
|
1704
|
+
const obtainedAt = storedTokens ? JSON.parse(storedTokens).obtainedAt : undefined;
|
|
1705
|
+
resolvedMarkers[key] = obtainedAt != null ? `oauth:${serverName}:${obtainedAt}` : value;
|
|
1706
|
+
} catch {
|
|
1707
|
+
resolvedMarkers[key] = value;
|
|
1708
|
+
}
|
|
1709
|
+
} else if (value.startsWith("secret://")) {
|
|
1344
1710
|
try {
|
|
1345
1711
|
resolvedMarkers[key] = secrets.resolveMaybeSecret(value);
|
|
1346
1712
|
} catch {
|
|
@@ -1355,6 +1721,9 @@ function specFingerprint(spec, secrets) {
|
|
|
1355
1721
|
collect(spec.headers);
|
|
1356
1722
|
return JSON.stringify({ spec, resolvedMarkers });
|
|
1357
1723
|
}
|
|
1724
|
+
function passthroughCacheSuffix(passthroughAuthorizationHeader) {
|
|
1725
|
+
return `passthrough:${crypto2.createHash("sha256").update(passthroughAuthorizationHeader).digest("hex").slice(0, 16)}`;
|
|
1726
|
+
}
|
|
1358
1727
|
var EXTRA_INHERITED_ENV = ["TMPDIR", "LANG"];
|
|
1359
1728
|
function resolveStdioEnv(spec, secrets) {
|
|
1360
1729
|
const env = getDefaultEnvironment();
|
|
@@ -1405,8 +1774,8 @@ async function closeUpstreamConnection(entry) {
|
|
|
1405
1774
|
await connection.transport.close();
|
|
1406
1775
|
} catch {}
|
|
1407
1776
|
}
|
|
1408
|
-
async function reconcileUpstreamConnections(config, runtime) {
|
|
1409
|
-
const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec)]));
|
|
1777
|
+
async function reconcileUpstreamConnections(config, secrets, runtime) {
|
|
1778
|
+
const activeSpecs = new Map(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name, spec]) => [name, specFingerprint(spec, secrets)]));
|
|
1410
1779
|
const staleKeys = [];
|
|
1411
1780
|
for (const [key, entry] of runtime.upstreamConnections.entries()) {
|
|
1412
1781
|
const upstreamName = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
|
|
@@ -1425,7 +1794,7 @@ async function reconcileUpstreamConnections(config, runtime) {
|
|
|
1425
1794
|
}
|
|
1426
1795
|
}
|
|
1427
1796
|
function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHeader, runtime) {
|
|
1428
|
-
const key = passthroughAuthorizationHeader ? `${upstreamName}
|
|
1797
|
+
const key = passthroughAuthorizationHeader ? `${upstreamName}:${passthroughCacheSuffix(passthroughAuthorizationHeader)}` : upstreamName;
|
|
1429
1798
|
const existing = runtime.upstreamConnections.get(key);
|
|
1430
1799
|
if (!existing) {
|
|
1431
1800
|
return;
|
|
@@ -1435,8 +1804,8 @@ function invalidateUpstreamConnection(upstreamName, passthroughAuthorizationHead
|
|
|
1435
1804
|
}
|
|
1436
1805
|
async function getUpstreamConnection(upstream, secrets, runtime, options = {}) {
|
|
1437
1806
|
const { forceOAuthRefresh = false, passthroughAuthorizationHeader } = options;
|
|
1438
|
-
const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}
|
|
1439
|
-
const fingerprint = specFingerprint(upstream.spec);
|
|
1807
|
+
const cacheKey = passthroughAuthorizationHeader ? `${upstream.name}:${passthroughCacheSuffix(passthroughAuthorizationHeader)}` : upstream.name;
|
|
1808
|
+
const fingerprint = specFingerprint(upstream.spec, secrets);
|
|
1440
1809
|
if (!forceOAuthRefresh) {
|
|
1441
1810
|
const existing = runtime.upstreamConnections.get(cacheKey);
|
|
1442
1811
|
if (existing) {
|
|
@@ -1515,12 +1884,20 @@ async function callUpstreamOnce(upstream, method, params, secrets, runtime, pass
|
|
|
1515
1884
|
throw classifyUpstreamError(upstream.name, error, wwwAuthenticate);
|
|
1516
1885
|
}
|
|
1517
1886
|
}
|
|
1887
|
+
function tokensMatch(candidate, expectedToken) {
|
|
1888
|
+
if (!candidate) {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
const candidateBuf = Buffer.from(candidate);
|
|
1892
|
+
const expectedBuf = Buffer.from(expectedToken);
|
|
1893
|
+
return candidateBuf.length === expectedBuf.length && crypto2.timingSafeEqual(candidateBuf, expectedBuf);
|
|
1894
|
+
}
|
|
1518
1895
|
function authHeaderIsValid(request, expectedToken) {
|
|
1519
1896
|
const localTokenHeader = request.headers["x-mcpx-local-token"];
|
|
1520
|
-
if (typeof localTokenHeader === "string" && localTokenHeader
|
|
1897
|
+
if (typeof localTokenHeader === "string" && tokensMatch(localTokenHeader, expectedToken)) {
|
|
1521
1898
|
return true;
|
|
1522
1899
|
}
|
|
1523
|
-
if (Array.isArray(localTokenHeader) && localTokenHeader.
|
|
1900
|
+
if (Array.isArray(localTokenHeader) && localTokenHeader.some((value) => tokensMatch(value, expectedToken))) {
|
|
1524
1901
|
return true;
|
|
1525
1902
|
}
|
|
1526
1903
|
const authHeader = request.headers.authorization;
|
|
@@ -1528,7 +1905,7 @@ function authHeaderIsValid(request, expectedToken) {
|
|
|
1528
1905
|
return false;
|
|
1529
1906
|
}
|
|
1530
1907
|
const [scheme, token] = authHeader.split(" ");
|
|
1531
|
-
return scheme === "Bearer" && token
|
|
1908
|
+
return scheme === "Bearer" && tokensMatch(token, expectedToken);
|
|
1532
1909
|
}
|
|
1533
1910
|
function isAuthChallenge(error) {
|
|
1534
1911
|
return error instanceof UpstreamError && (error.code === "auth_required" || error.code === "auth_expired");
|
|
@@ -1539,7 +1916,7 @@ function getClientAuthorizationForUpstream(request, expectedToken) {
|
|
|
1539
1916
|
return;
|
|
1540
1917
|
}
|
|
1541
1918
|
const [scheme, token] = authHeader.split(" ");
|
|
1542
|
-
if (scheme === "Bearer" && token
|
|
1919
|
+
if (scheme === "Bearer" && tokensMatch(token, expectedToken)) {
|
|
1543
1920
|
return;
|
|
1544
1921
|
}
|
|
1545
1922
|
return authHeader;
|
|
@@ -1556,11 +1933,11 @@ async function handleListTools(config, secrets, runtime, upstreamFilter, clientA
|
|
|
1556
1933
|
runtime.tokenCache = new Map;
|
|
1557
1934
|
}
|
|
1558
1935
|
const cached = runtime.tokenCache.get(upstream.name);
|
|
1559
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1936
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1560
1937
|
existing.tools = Math.ceil(JSON.stringify(result.tools ?? []).length / 4);
|
|
1561
1938
|
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1562
1939
|
delete existing.error;
|
|
1563
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
1940
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
|
|
1564
1941
|
for (const tool of result.tools ?? []) {
|
|
1565
1942
|
const name = typeof tool.name === "string" ? tool.name : "tool";
|
|
1566
1943
|
tools.push({
|
|
@@ -1594,11 +1971,11 @@ async function handleListResources(config, secrets, runtime, upstreamFilter, cli
|
|
|
1594
1971
|
runtime.tokenCache = new Map;
|
|
1595
1972
|
}
|
|
1596
1973
|
const cached = runtime.tokenCache.get(upstream.name);
|
|
1597
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1974
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1598
1975
|
existing.resources = Math.ceil(JSON.stringify(result.resources ?? []).length / 4);
|
|
1599
1976
|
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1600
1977
|
delete existing.error;
|
|
1601
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
1978
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
|
|
1602
1979
|
for (const resource of result.resources ?? []) {
|
|
1603
1980
|
const originalUri = typeof resource.uri === "string" ? resource.uri : "";
|
|
1604
1981
|
const originalName = typeof resource.name === "string" ? resource.name : originalUri;
|
|
@@ -1634,11 +2011,11 @@ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clien
|
|
|
1634
2011
|
runtime.tokenCache = new Map;
|
|
1635
2012
|
}
|
|
1636
2013
|
const cached = runtime.tokenCache.get(upstream.name);
|
|
1637
|
-
const existing = cached?.fingerprint === specFingerprint(upstream.spec) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
2014
|
+
const existing = cached?.fingerprint === specFingerprint(upstream.spec, secrets) ? cached.count : { tools: 0, resources: 0, prompts: 0, total: 0 };
|
|
1638
2015
|
existing.prompts = Math.ceil(JSON.stringify(result.prompts ?? []).length / 4);
|
|
1639
2016
|
existing.total = existing.tools + existing.resources + existing.prompts;
|
|
1640
2017
|
delete existing.error;
|
|
1641
|
-
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec), count: existing });
|
|
2018
|
+
runtime.tokenCache.set(upstream.name, { fingerprint: specFingerprint(upstream.spec, secrets), count: existing });
|
|
1642
2019
|
for (const prompt of result.prompts ?? []) {
|
|
1643
2020
|
const name = typeof prompt.name === "string" ? prompt.name : "prompt";
|
|
1644
2021
|
prompts.push({
|
|
@@ -1799,7 +2176,7 @@ async function handleRequestObject(request, secrets, runtime, upstreamFilter, cl
|
|
|
1799
2176
|
if (upstreamFilter && listUpstreams(config, upstreamFilter).length === 0) {
|
|
1800
2177
|
return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
|
|
1801
2178
|
}
|
|
1802
|
-
await reconcileUpstreamConnections(config, runtime);
|
|
2179
|
+
await reconcileUpstreamConnections(config, secrets, runtime);
|
|
1803
2180
|
if (request.method === "custom/tokenCounts") {
|
|
1804
2181
|
const result = await getUpstreamTokenCounts(config, secrets, runtime);
|
|
1805
2182
|
return makeResult(id, result);
|
|
@@ -1862,12 +2239,36 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
|
|
|
1862
2239
|
headers["mcp-protocol-version"] = protocolVersion;
|
|
1863
2240
|
}
|
|
1864
2241
|
for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
|
|
1865
|
-
|
|
2242
|
+
if (key.toLowerCase() === "authorization" && isOAuthReference(value)) {
|
|
2243
|
+
try {
|
|
2244
|
+
const accessToken = await getOAuthAccessToken(oauthReferenceServerName(value), upstream.spec, secrets);
|
|
2245
|
+
headers[key] = `Bearer ${accessToken}`;
|
|
2246
|
+
} catch {}
|
|
2247
|
+
} else {
|
|
2248
|
+
headers[key] = secrets.resolveMaybeSecret(value);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
const timeoutController = new AbortController;
|
|
2252
|
+
const timeoutHandle = setTimeout(() => timeoutController.abort(), DEFAULT_CONNECT_TIMEOUT_MS);
|
|
2253
|
+
let upstreamResponse;
|
|
2254
|
+
try {
|
|
2255
|
+
upstreamResponse = await fetch(upstreamWellKnownUrl, {
|
|
2256
|
+
method: "GET",
|
|
2257
|
+
headers,
|
|
2258
|
+
signal: timeoutController.signal
|
|
2259
|
+
});
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
const isAbort = error.name === "AbortError";
|
|
2262
|
+
response.statusCode = 502;
|
|
2263
|
+
response.setHeader("content-type", "application/json");
|
|
2264
|
+
response.end(JSON.stringify({
|
|
2265
|
+
error: "upstream_unreachable",
|
|
2266
|
+
message: isAbort ? `Well-known request timed out after ${DEFAULT_CONNECT_TIMEOUT_MS}ms.` : error instanceof Error ? error.message : String(error)
|
|
2267
|
+
}));
|
|
2268
|
+
return true;
|
|
2269
|
+
} finally {
|
|
2270
|
+
clearTimeout(timeoutHandle);
|
|
1866
2271
|
}
|
|
1867
|
-
const upstreamResponse = await fetch(upstreamWellKnownUrl, {
|
|
1868
|
-
method: "GET",
|
|
1869
|
-
headers
|
|
1870
|
-
});
|
|
1871
2272
|
let bodyText = await upstreamResponse.text();
|
|
1872
2273
|
response.statusCode = upstreamResponse.status;
|
|
1873
2274
|
const contentType = upstreamResponse.headers.get("content-type");
|
|
@@ -1892,6 +2293,16 @@ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, s
|
|
|
1892
2293
|
response.end(bodyText);
|
|
1893
2294
|
return true;
|
|
1894
2295
|
}
|
|
2296
|
+
var SENSITIVE_HEADER_NAMES = new Set(["authorization", "x-mcpx-local-token"]);
|
|
2297
|
+
function redactSensitiveHeaders(headers) {
|
|
2298
|
+
const redacted = { ...headers };
|
|
2299
|
+
for (const name of Object.keys(redacted)) {
|
|
2300
|
+
if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
|
|
2301
|
+
redacted[name] = "[redacted]";
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return redacted;
|
|
2305
|
+
}
|
|
1895
2306
|
function createGatewayServer(options) {
|
|
1896
2307
|
const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
|
|
1897
2308
|
const runtime = {
|
|
@@ -1905,7 +2316,7 @@ function createGatewayServer(options) {
|
|
|
1905
2316
|
const upstreamFilter = getRequestedUpstream(requestUrl);
|
|
1906
2317
|
if (debug) {
|
|
1907
2318
|
console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
|
|
1908
|
-
console.error(`[mcpx gateway] headers=${JSON.stringify(request.headers)}`);
|
|
2319
|
+
console.error(`[mcpx gateway] headers=${JSON.stringify(redactSensitiveHeaders(request.headers))}`);
|
|
1909
2320
|
}
|
|
1910
2321
|
if (await maybeHandleWellKnownOAuthRequest(request, response, requestUrl, options.secrets)) {
|
|
1911
2322
|
if (debug) {
|
|
@@ -2089,10 +2500,10 @@ function buildDaemonChildEnv(baseEnv = process.env, electronRuntime = typeof pro
|
|
|
2089
2500
|
};
|
|
2090
2501
|
}
|
|
2091
2502
|
function readPidRecordFromFile(pidPath = getPidPath()) {
|
|
2092
|
-
if (!
|
|
2503
|
+
if (!fs6.existsSync(pidPath)) {
|
|
2093
2504
|
return null;
|
|
2094
2505
|
}
|
|
2095
|
-
const raw =
|
|
2506
|
+
const raw = fs6.readFileSync(pidPath, "utf8").trim();
|
|
2096
2507
|
if (!raw) {
|
|
2097
2508
|
return null;
|
|
2098
2509
|
}
|
|
@@ -2152,7 +2563,7 @@ async function resolveGatewayPort(config, secrets) {
|
|
|
2152
2563
|
}
|
|
2153
2564
|
throw new Error(`No available local port found near ${config.gateway.port}.`);
|
|
2154
2565
|
}
|
|
2155
|
-
async function waitForGatewayReady(port, token, timeoutMs =
|
|
2566
|
+
async function waitForGatewayReady(port, token, timeoutMs = 15000) {
|
|
2156
2567
|
const deadline = Date.now() + timeoutMs;
|
|
2157
2568
|
let lastError;
|
|
2158
2569
|
while (Date.now() < deadline) {
|
|
@@ -2200,18 +2611,12 @@ function getDaemonStatus(config) {
|
|
|
2200
2611
|
async function startDaemon(config, cliPath, secrets) {
|
|
2201
2612
|
const existingStatus = getDaemonStatus(config);
|
|
2202
2613
|
if (existingStatus.running && existingStatus.pid) {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
message: "mcpx daemon already running."
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
try {
|
|
2213
|
-
fs5.unlinkSync(getPidPath());
|
|
2214
|
-
} catch {}
|
|
2614
|
+
return {
|
|
2615
|
+
started: false,
|
|
2616
|
+
pid: existingStatus.pid,
|
|
2617
|
+
port: existingStatus.port ?? config.gateway.port,
|
|
2618
|
+
message: "mcpx daemon already running."
|
|
2619
|
+
};
|
|
2215
2620
|
}
|
|
2216
2621
|
const { port, fellBackFrom } = await resolveGatewayPort(config, secrets);
|
|
2217
2622
|
if (fellBackFrom) {
|
|
@@ -2223,24 +2628,24 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
2223
2628
|
const logPath = getLogPath();
|
|
2224
2629
|
ensureParentDir(pidPath);
|
|
2225
2630
|
ensureParentDir(logPath);
|
|
2226
|
-
let logFd =
|
|
2227
|
-
const logStat =
|
|
2631
|
+
let logFd = fs6.openSync(logPath, "a", 384);
|
|
2632
|
+
const logStat = fs6.fstatSync(logFd);
|
|
2228
2633
|
if (logStat.size > 10 * 1024 * 1024) {
|
|
2229
|
-
|
|
2634
|
+
fs6.closeSync(logFd);
|
|
2230
2635
|
const log1 = `${logPath}.1`;
|
|
2231
2636
|
const log2 = `${logPath}.2`;
|
|
2232
2637
|
try {
|
|
2233
|
-
if (
|
|
2234
|
-
|
|
2638
|
+
if (fs6.existsSync(log2))
|
|
2639
|
+
fs6.unlinkSync(log2);
|
|
2235
2640
|
} catch {}
|
|
2236
2641
|
try {
|
|
2237
|
-
if (
|
|
2238
|
-
|
|
2642
|
+
if (fs6.existsSync(log1))
|
|
2643
|
+
fs6.renameSync(log1, log2);
|
|
2239
2644
|
} catch {}
|
|
2240
2645
|
try {
|
|
2241
|
-
|
|
2646
|
+
fs6.renameSync(logPath, log1);
|
|
2242
2647
|
} catch {}
|
|
2243
|
-
logFd =
|
|
2648
|
+
logFd = fs6.openSync(logPath, "a", 384);
|
|
2244
2649
|
}
|
|
2245
2650
|
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
2246
2651
|
detached: true,
|
|
@@ -2248,7 +2653,7 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
2248
2653
|
env: buildDaemonChildEnv()
|
|
2249
2654
|
});
|
|
2250
2655
|
child.unref();
|
|
2251
|
-
|
|
2656
|
+
fs6.writeFileSync(pidPath, `${child.pid}:${port}
|
|
2252
2657
|
`, { mode: 384 });
|
|
2253
2658
|
startBackgroundUpdateCheck();
|
|
2254
2659
|
await waitForGatewayReady(port, token);
|
|
@@ -2272,7 +2677,7 @@ function stopDaemon() {
|
|
|
2272
2677
|
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
|
|
2273
2678
|
const looksLikeMcpxDaemon = cmd.includes("daemon run") && (cmd.includes("mcpx") || cmd.includes("cli.js"));
|
|
2274
2679
|
if (!looksLikeMcpxDaemon) {
|
|
2275
|
-
|
|
2680
|
+
fs6.unlinkSync(pidPath);
|
|
2276
2681
|
return {
|
|
2277
2682
|
stopped: false,
|
|
2278
2683
|
message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
|
|
@@ -2280,7 +2685,7 @@ function stopDaemon() {
|
|
|
2280
2685
|
}
|
|
2281
2686
|
} catch {
|
|
2282
2687
|
try {
|
|
2283
|
-
|
|
2688
|
+
fs6.unlinkSync(pidPath);
|
|
2284
2689
|
} catch {}
|
|
2285
2690
|
return {
|
|
2286
2691
|
stopped: false,
|
|
@@ -2291,7 +2696,7 @@ function stopDaemon() {
|
|
|
2291
2696
|
process.kill(pid, "SIGTERM");
|
|
2292
2697
|
} catch {}
|
|
2293
2698
|
try {
|
|
2294
|
-
|
|
2699
|
+
fs6.unlinkSync(pidPath);
|
|
2295
2700
|
} catch {}
|
|
2296
2701
|
return {
|
|
2297
2702
|
stopped: true,
|
|
@@ -2304,10 +2709,10 @@ async function restartDaemon(config, cliPath, secrets) {
|
|
|
2304
2709
|
}
|
|
2305
2710
|
function readDaemonLogs(maxLines = 200) {
|
|
2306
2711
|
const logPath = getLogPath();
|
|
2307
|
-
if (!
|
|
2712
|
+
if (!fs6.existsSync(logPath)) {
|
|
2308
2713
|
return "";
|
|
2309
2714
|
}
|
|
2310
|
-
const lines =
|
|
2715
|
+
const lines = fs6.readFileSync(logPath, "utf8").split(`
|
|
2311
2716
|
`);
|
|
2312
2717
|
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
2313
2718
|
`);
|
|
@@ -2452,4 +2857,4 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
2452
2857
|
};
|
|
2453
2858
|
}
|
|
2454
2859
|
|
|
2455
|
-
export { probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath,
|
|
2860
|
+
export { isOAuthReference, oauthReferenceServerName, __resetOAuthSupportCache, probeOAuthSupport, OAUTH_LOGIN_TIMEOUT_MS, OAuthCancelledError, OAuthLoginInProgressError, runOAuthLogin, getOAuthAccessToken, oauthSecretNames, clearOAuthCredentials, getOAuthCredentialStatus, probeHttpAuthRequirement, fetchRegistryServerDetail, selectBestPackage, extractRequiredInputs, mapRegistryToSpec, toSecretRef, secretRefName, defaultAuthSecretName, resolveAuthTarget, applyAuthReference, removeAuthReference, listAuthBindings, maybePrefixBearer, buildEnrichedPath, compareVersions, checkForUpdates, getStagedUpdate, shouldUseStagedCli, clearStagedUpdate, getStagedCliPath, startBackgroundUpdateCheck, runBackgroundUpdate, performUpdate, performRollback, runMarketplaceAutoUpdate, startMarketplaceAutoUpdater, getDaemonStatus, startDaemon, stopDaemon, restartDaemon, readDaemonLogs, runDaemonForeground, STATUS_CLIENTS, buildStatusReport };
|