@kwonye/mcpx 0.1.99 → 0.1.101
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-7x2n23em.js → cli-qzzjkt0q.js} +1132 -723
- 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 +1 -0
- 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
|
-
return {
|
|
120
|
-
authRequired: false,
|
|
121
|
-
status: response.status
|
|
122
|
-
};
|
|
123
|
-
} catch (error) {
|
|
124
|
-
const isAbort = error.name === "AbortError";
|
|
125
202
|
return {
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
const inputs = [];
|
|
178
|
-
if (option.kind === "package" && option.package?.environmentVariables) {
|
|
179
|
-
for (const env of option.package.environmentVariables) {
|
|
180
|
-
if (env.isRequired && !env.default) {
|
|
181
|
-
inputs.push({
|
|
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
|
-
}
|
|
198
|
-
}
|
|
228
|
+
redirectToAuthorization(_authorizationUrl) {}
|
|
229
|
+
saveCodeVerifier(codeVerifier) {
|
|
230
|
+
this.#secrets.setSecret(oauthSecretName(this.#serverName, "verifier"), codeVerifier);
|
|
199
231
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
name: header.name,
|
|
205
|
-
description: header.description,
|
|
206
|
-
isSecret: header.isSecret ?? false,
|
|
207
|
-
kind: "header"
|
|
208
|
-
});
|
|
209
|
-
}
|
|
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}".`);
|
|
210
236
|
}
|
|
237
|
+
return verifier;
|
|
211
238
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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));
|
|
221
250
|
}
|
|
222
|
-
return {
|
|
223
|
-
transport: "http",
|
|
224
|
-
url: option.remote.url,
|
|
225
|
-
...Object.keys(headers).length > 0 ? { headers } : {}
|
|
226
|
-
};
|
|
227
251
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
252
|
+
}
|
|
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;
|
|
261
|
+
}
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
saveTokens(tokens) {
|
|
488
|
-
writeJsonSecret(this.#secrets, oauthSecretName(this.#serverName, "tokens"), {
|
|
489
|
-
tokens,
|
|
490
|
-
obtainedAt: Date.now()
|
|
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
|
|
491
754
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
throw new Error(`Missing OAuth code verifier for "${this.#serverName}".`);
|
|
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
|
+
});
|
|
501
763
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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 {}
|
|
514
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);
|
|
515
792
|
}
|
|
516
793
|
}
|
|
517
794
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
await this.#openUrl(authorizationUrl.toString());
|
|
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}`);
|
|
528
804
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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] };
|
|
532
824
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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] };
|
|
536
828
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return this.#cachedCodeVerifier;
|
|
540
|
-
}
|
|
541
|
-
return super.codeVerifier();
|
|
829
|
+
if (packages.length > 0) {
|
|
830
|
+
return { kind: "package", package: packages[0] };
|
|
542
831
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
this.#cachedCodeVerifier = codeVerifier;
|
|
832
|
+
if (remotes.length > 0) {
|
|
833
|
+
return { kind: "remote", remote: remotes[0] };
|
|
546
834
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
}
|
|
551
859
|
}
|
|
552
860
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
+
});
|
|
562
870
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return inputs;
|
|
566
874
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
};
|
|
888
|
+
}
|
|
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);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
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;
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
transport: "stdio",
|
|
912
|
+
command: runtime,
|
|
913
|
+
args,
|
|
914
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
915
|
+
};
|
|
571
916
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
});
|
|
917
|
+
|
|
918
|
+
// src/core/server-auth.ts
|
|
919
|
+
function normalizeNamePart(value) {
|
|
920
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "value";
|
|
607
921
|
}
|
|
608
|
-
function
|
|
609
|
-
const
|
|
610
|
-
if (!
|
|
611
|
-
throw new Error(
|
|
922
|
+
function requireNonEmpty(value, label) {
|
|
923
|
+
const normalized = value?.trim();
|
|
924
|
+
if (!normalized) {
|
|
925
|
+
throw new Error(`${label} must be non-empty.`);
|
|
612
926
|
}
|
|
613
|
-
|
|
614
|
-
|
|
927
|
+
return normalized;
|
|
928
|
+
}
|
|
929
|
+
function toSecretRef(secretName) {
|
|
930
|
+
return `secret://${secretName}`;
|
|
931
|
+
}
|
|
932
|
+
function secretRefName(ref) {
|
|
933
|
+
if (!ref.startsWith("secret://")) {
|
|
934
|
+
return null;
|
|
615
935
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
936
|
+
const name = ref.slice("secret://".length).trim();
|
|
937
|
+
return name.length > 0 ? name : null;
|
|
938
|
+
}
|
|
939
|
+
function defaultAuthSecretName(serverName, target) {
|
|
940
|
+
return `auth_${normalizeNamePart(serverName)}_${target.kind}_${normalizeNamePart(target.key)}`;
|
|
941
|
+
}
|
|
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
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (header) {
|
|
953
|
+
throw new Error("--header is only valid for HTTP upstreams.");
|
|
954
|
+
}
|
|
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.`);
|
|
677
993
|
}
|
|
994
|
+
if (!spec.env || !(target.key in spec.env)) {
|
|
995
|
+
return null;
|
|
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) {
|
|
@@ -2081,11 +2492,18 @@ function createGatewayServer(options) {
|
|
|
2081
2492
|
}
|
|
2082
2493
|
|
|
2083
2494
|
// src/core/daemon.ts
|
|
2495
|
+
function buildDaemonChildEnv(baseEnv = process.env, electronRuntime = typeof process.versions.electron === "string") {
|
|
2496
|
+
return {
|
|
2497
|
+
...baseEnv,
|
|
2498
|
+
MCPX_DAEMON_CHILD: "1",
|
|
2499
|
+
...electronRuntime ? { ELECTRON_RUN_AS_NODE: "1" } : {}
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2084
2502
|
function readPidRecordFromFile(pidPath = getPidPath()) {
|
|
2085
|
-
if (!
|
|
2503
|
+
if (!fs6.existsSync(pidPath)) {
|
|
2086
2504
|
return null;
|
|
2087
2505
|
}
|
|
2088
|
-
const raw =
|
|
2506
|
+
const raw = fs6.readFileSync(pidPath, "utf8").trim();
|
|
2089
2507
|
if (!raw) {
|
|
2090
2508
|
return null;
|
|
2091
2509
|
}
|
|
@@ -2145,7 +2563,7 @@ async function resolveGatewayPort(config, secrets) {
|
|
|
2145
2563
|
}
|
|
2146
2564
|
throw new Error(`No available local port found near ${config.gateway.port}.`);
|
|
2147
2565
|
}
|
|
2148
|
-
async function waitForGatewayReady(port, token, timeoutMs =
|
|
2566
|
+
async function waitForGatewayReady(port, token, timeoutMs = 15000) {
|
|
2149
2567
|
const deadline = Date.now() + timeoutMs;
|
|
2150
2568
|
let lastError;
|
|
2151
2569
|
while (Date.now() < deadline) {
|
|
@@ -2193,18 +2611,12 @@ function getDaemonStatus(config) {
|
|
|
2193
2611
|
async function startDaemon(config, cliPath, secrets) {
|
|
2194
2612
|
const existingStatus = getDaemonStatus(config);
|
|
2195
2613
|
if (existingStatus.running && existingStatus.pid) {
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
message: "mcpx daemon already running."
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
try {
|
|
2206
|
-
fs5.unlinkSync(getPidPath());
|
|
2207
|
-
} catch {}
|
|
2614
|
+
return {
|
|
2615
|
+
started: false,
|
|
2616
|
+
pid: existingStatus.pid,
|
|
2617
|
+
port: existingStatus.port ?? config.gateway.port,
|
|
2618
|
+
message: "mcpx daemon already running."
|
|
2619
|
+
};
|
|
2208
2620
|
}
|
|
2209
2621
|
const { port, fellBackFrom } = await resolveGatewayPort(config, secrets);
|
|
2210
2622
|
if (fellBackFrom) {
|
|
@@ -2216,35 +2628,32 @@ async function startDaemon(config, cliPath, secrets) {
|
|
|
2216
2628
|
const logPath = getLogPath();
|
|
2217
2629
|
ensureParentDir(pidPath);
|
|
2218
2630
|
ensureParentDir(logPath);
|
|
2219
|
-
let logFd =
|
|
2220
|
-
const logStat =
|
|
2631
|
+
let logFd = fs6.openSync(logPath, "a", 384);
|
|
2632
|
+
const logStat = fs6.fstatSync(logFd);
|
|
2221
2633
|
if (logStat.size > 10 * 1024 * 1024) {
|
|
2222
|
-
|
|
2634
|
+
fs6.closeSync(logFd);
|
|
2223
2635
|
const log1 = `${logPath}.1`;
|
|
2224
2636
|
const log2 = `${logPath}.2`;
|
|
2225
2637
|
try {
|
|
2226
|
-
if (
|
|
2227
|
-
|
|
2638
|
+
if (fs6.existsSync(log2))
|
|
2639
|
+
fs6.unlinkSync(log2);
|
|
2228
2640
|
} catch {}
|
|
2229
2641
|
try {
|
|
2230
|
-
if (
|
|
2231
|
-
|
|
2642
|
+
if (fs6.existsSync(log1))
|
|
2643
|
+
fs6.renameSync(log1, log2);
|
|
2232
2644
|
} catch {}
|
|
2233
2645
|
try {
|
|
2234
|
-
|
|
2646
|
+
fs6.renameSync(logPath, log1);
|
|
2235
2647
|
} catch {}
|
|
2236
|
-
logFd =
|
|
2648
|
+
logFd = fs6.openSync(logPath, "a", 384);
|
|
2237
2649
|
}
|
|
2238
2650
|
const child = spawn2(process.execPath, [cliPath, "daemon", "run", "--port", String(port)], {
|
|
2239
2651
|
detached: true,
|
|
2240
2652
|
stdio: ["ignore", logFd, logFd],
|
|
2241
|
-
env:
|
|
2242
|
-
...process.env,
|
|
2243
|
-
MCPX_DAEMON_CHILD: "1"
|
|
2244
|
-
}
|
|
2653
|
+
env: buildDaemonChildEnv()
|
|
2245
2654
|
});
|
|
2246
2655
|
child.unref();
|
|
2247
|
-
|
|
2656
|
+
fs6.writeFileSync(pidPath, `${child.pid}:${port}
|
|
2248
2657
|
`, { mode: 384 });
|
|
2249
2658
|
startBackgroundUpdateCheck();
|
|
2250
2659
|
await waitForGatewayReady(port, token);
|
|
@@ -2268,7 +2677,7 @@ function stopDaemon() {
|
|
|
2268
2677
|
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 2000 }).trim();
|
|
2269
2678
|
const looksLikeMcpxDaemon = cmd.includes("daemon run") && (cmd.includes("mcpx") || cmd.includes("cli.js"));
|
|
2270
2679
|
if (!looksLikeMcpxDaemon) {
|
|
2271
|
-
|
|
2680
|
+
fs6.unlinkSync(pidPath);
|
|
2272
2681
|
return {
|
|
2273
2682
|
stopped: false,
|
|
2274
2683
|
message: `PID ${pid} is not a mcpx daemon (command: ${cmd.slice(0, 80)}). Pidfile cleaned up.`
|
|
@@ -2276,7 +2685,7 @@ function stopDaemon() {
|
|
|
2276
2685
|
}
|
|
2277
2686
|
} catch {
|
|
2278
2687
|
try {
|
|
2279
|
-
|
|
2688
|
+
fs6.unlinkSync(pidPath);
|
|
2280
2689
|
} catch {}
|
|
2281
2690
|
return {
|
|
2282
2691
|
stopped: false,
|
|
@@ -2287,7 +2696,7 @@ function stopDaemon() {
|
|
|
2287
2696
|
process.kill(pid, "SIGTERM");
|
|
2288
2697
|
} catch {}
|
|
2289
2698
|
try {
|
|
2290
|
-
|
|
2699
|
+
fs6.unlinkSync(pidPath);
|
|
2291
2700
|
} catch {}
|
|
2292
2701
|
return {
|
|
2293
2702
|
stopped: true,
|
|
@@ -2300,10 +2709,10 @@ async function restartDaemon(config, cliPath, secrets) {
|
|
|
2300
2709
|
}
|
|
2301
2710
|
function readDaemonLogs(maxLines = 200) {
|
|
2302
2711
|
const logPath = getLogPath();
|
|
2303
|
-
if (!
|
|
2712
|
+
if (!fs6.existsSync(logPath)) {
|
|
2304
2713
|
return "";
|
|
2305
2714
|
}
|
|
2306
|
-
const lines =
|
|
2715
|
+
const lines = fs6.readFileSync(logPath, "utf8").split(`
|
|
2307
2716
|
`);
|
|
2308
2717
|
return lines.slice(Math.max(lines.length - maxLines, 0)).join(`
|
|
2309
2718
|
`);
|
|
@@ -2448,4 +2857,4 @@ async function buildStatusReport(config, managedIndex, daemon = getDaemonStatus(
|
|
|
2448
2857
|
};
|
|
2449
2858
|
}
|
|
2450
2859
|
|
|
2451
|
-
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 };
|