mcp-server-bitbucket 0.12.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +9 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +838 -139
- package/package.json +14 -12
- package/scripts/check-ca-bundles.cjs +27 -0
- package/smithery.yaml +10 -4
- package/src/auth.ts +330 -0
- package/src/client.ts +215 -123
- package/src/http-input.ts +36 -0
- package/src/index.ts +274 -13
- package/src/oauth-proxy.ts +298 -0
- package/src/prompts.ts +1 -1
- package/src/settings.ts +51 -10
- package/src/tools/commits.ts +65 -33
- package/src/tools/index.ts +0 -3
- package/src/tools/source.ts +23 -0
- package/src/tools/tags.ts +21 -6
- package/tests/auth.test.ts +277 -0
- package/tests/history.test.ts +284 -0
- package/tests/http-handler.test.ts +67 -0
- package/tests/http-input.test.ts +40 -0
- package/tests/network-resilience.test.ts +113 -0
- package/tests/oauth-proxy.test.ts +286 -0
- package/tests/settings.test.ts +110 -0
- package/tests/stdio.test.ts +20 -0
- package/tests/test-remote.mjs +22 -0
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
7
|
import {
|
|
7
8
|
CallToolRequestSchema,
|
|
8
9
|
ListToolsRequestSchema,
|
|
@@ -11,26 +12,374 @@ import {
|
|
|
11
12
|
ListPromptsRequestSchema,
|
|
12
13
|
GetPromptRequestSchema
|
|
13
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { createServer as createHttpServer } from "http";
|
|
16
|
+
import { pathToFileURL } from "url";
|
|
17
|
+
import { realpathSync } from "fs";
|
|
18
|
+
|
|
19
|
+
// src/http-input.ts
|
|
20
|
+
var HttpInputError = class extends Error {
|
|
21
|
+
constructor(message, status = 400) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.status = status;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
async function readHttpBody(source) {
|
|
27
|
+
const chunks = [];
|
|
28
|
+
let bytes = 0;
|
|
29
|
+
for await (const chunk of source) {
|
|
30
|
+
bytes += chunk.byteLength;
|
|
31
|
+
if (bytes > 100 * 1024) throw new HttpInputError("Request body exceeds 100 KiB", 413);
|
|
32
|
+
chunks.push(Buffer.from(chunk));
|
|
33
|
+
}
|
|
34
|
+
return Buffer.concat(chunks);
|
|
35
|
+
}
|
|
36
|
+
function parseMcpBody(body) {
|
|
37
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
38
|
+
if (Array.isArray(parsed)) throw new HttpInputError("JSON-RPC batches are not supported");
|
|
39
|
+
if (parsed && typeof parsed === "object" && "method" in parsed && parsed.method === "tools/call") {
|
|
40
|
+
const pending = [parsed];
|
|
41
|
+
let elements = 0;
|
|
42
|
+
while (pending.length) {
|
|
43
|
+
const value = pending.pop();
|
|
44
|
+
if (value && typeof value === "object") {
|
|
45
|
+
const values = Object.values(value);
|
|
46
|
+
elements += values.length;
|
|
47
|
+
if (elements > 1e3) throw new HttpInputError("Tool input exceeds 1000 elements");
|
|
48
|
+
pending.push(...values);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/auth.ts
|
|
56
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
57
|
+
function defaultJwksUri(issuer) {
|
|
58
|
+
const base = issuer.replace(/\/+$/, "").replace(/\/v2\.0$/, "");
|
|
59
|
+
return `${base}/discovery/v2.0/keys`;
|
|
60
|
+
}
|
|
61
|
+
function getAuthConfig(env = process.env, jwksOverride) {
|
|
62
|
+
const issuer = (env.MCP_OAUTH_ISSUER || "").trim();
|
|
63
|
+
const audience = (env.MCP_OAUTH_AUDIENCE || "").trim();
|
|
64
|
+
if (!issuer || !audience) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const jwksUri = (env.MCP_OAUTH_JWKS_URI || "").trim() || defaultJwksUri(issuer);
|
|
68
|
+
const requiredScopes = parseScopes(env.MCP_OAUTH_REQUIRED_SCOPE);
|
|
69
|
+
const advertisedScopes = parseScopes(env.MCP_OAUTH_SCOPES_SUPPORTED) ?? requiredScopes;
|
|
70
|
+
const publicUrl = (env.MCP_PUBLIC_URL || "").trim().replace(/\/+$/, "");
|
|
71
|
+
const resourceUrl = publicUrl ? `${publicUrl}/mcp` : audience;
|
|
72
|
+
const resourceIdentifier = (env.MCP_OAUTH_RESOURCE || "").trim() || resourceUrl;
|
|
73
|
+
return {
|
|
74
|
+
issuer,
|
|
75
|
+
audience,
|
|
76
|
+
requiredScopes,
|
|
77
|
+
advertisedScopes,
|
|
78
|
+
resourceUrl,
|
|
79
|
+
resourceIdentifier,
|
|
80
|
+
proxyAuthorizationServerMetadata: (env.MCP_OAUTH_AS_METADATA || "proxy").trim() !== "issuer",
|
|
81
|
+
jwks: jwksOverride ?? createRemoteJWKSet(new URL(jwksUri))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function parseScopes(raw) {
|
|
85
|
+
const scopes = (raw || "").split(/[\s,]+/).filter(Boolean);
|
|
86
|
+
return scopes.length > 0 ? scopes : void 0;
|
|
87
|
+
}
|
|
88
|
+
function protectedResourceMetadata(config) {
|
|
89
|
+
const scopes = config.advertisedScopes ?? config.requiredScopes;
|
|
90
|
+
const authorizationServer = config.proxyAuthorizationServerMetadata ? serverOrigin(config) : config.issuer;
|
|
91
|
+
return {
|
|
92
|
+
resource: config.resourceIdentifier,
|
|
93
|
+
authorization_servers: [authorizationServer],
|
|
94
|
+
bearer_methods_supported: ["header"],
|
|
95
|
+
...scopes ? { scopes_supported: scopes } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
var METADATA_PATHS = [
|
|
99
|
+
"/.well-known/oauth-protected-resource/mcp",
|
|
100
|
+
"/.well-known/oauth-protected-resource"
|
|
101
|
+
];
|
|
102
|
+
function serverOrigin(config) {
|
|
103
|
+
return config.resourceUrl.replace(/\/mcp$/, "");
|
|
104
|
+
}
|
|
105
|
+
function metadataUrl(config) {
|
|
106
|
+
return `${serverOrigin(config)}${METADATA_PATHS[0]}`;
|
|
107
|
+
}
|
|
108
|
+
var AS_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
109
|
+
var asMetadataCache = null;
|
|
110
|
+
var AS_METADATA_TTL_MS = 60 * 60 * 1e3;
|
|
111
|
+
async function authorizationServerMetadata(config, fetchImpl = fetch, now = Date.now()) {
|
|
112
|
+
if (asMetadataCache && now - asMetadataCache.fetchedAt < AS_METADATA_TTL_MS) {
|
|
113
|
+
return asMetadataCache.document;
|
|
114
|
+
}
|
|
115
|
+
const url = `${config.issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`;
|
|
116
|
+
const response = await fetchImpl(url);
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Could not read OpenID configuration from ${url}: HTTP ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
const upstream = await response.json();
|
|
121
|
+
const document = {
|
|
122
|
+
...upstream,
|
|
123
|
+
code_challenge_methods_supported: upstream.code_challenge_methods_supported ?? ["S256"],
|
|
124
|
+
grant_types_supported: upstream.grant_types_supported ?? ["authorization_code", "refresh_token"]
|
|
125
|
+
};
|
|
126
|
+
asMetadataCache = { document, fetchedAt: now };
|
|
127
|
+
return document;
|
|
128
|
+
}
|
|
129
|
+
function wwwAuthenticate(config, failure) {
|
|
130
|
+
const parts = [
|
|
131
|
+
`Bearer realm="mcp"`,
|
|
132
|
+
`resource_metadata="${metadataUrl(config)}"`
|
|
133
|
+
];
|
|
134
|
+
if (failure) {
|
|
135
|
+
parts.push(`error="${failure.error}"`, `error_description="${failure.description}"`);
|
|
136
|
+
}
|
|
137
|
+
return parts.join(", ");
|
|
138
|
+
}
|
|
139
|
+
function tokenScopes(payload) {
|
|
140
|
+
const scp = payload.scp;
|
|
141
|
+
if (typeof scp === "string") {
|
|
142
|
+
return scp.split(" ").filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(scp)) {
|
|
145
|
+
return scp.filter((s) => typeof s === "string");
|
|
146
|
+
}
|
|
147
|
+
const roles = payload.roles;
|
|
148
|
+
if (Array.isArray(roles)) {
|
|
149
|
+
return roles.filter((r) => typeof r === "string");
|
|
150
|
+
}
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
async function verifyBearer(authorization, config) {
|
|
154
|
+
if (!authorization) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 401,
|
|
158
|
+
error: "invalid_token",
|
|
159
|
+
description: "Authorization header is required"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const match = /^Bearer[ ]+(.+)$/i.exec(authorization.trim());
|
|
163
|
+
if (!match) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
status: 401,
|
|
167
|
+
error: "invalid_token",
|
|
168
|
+
description: "Authorization header must use the Bearer scheme"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
let payload;
|
|
172
|
+
try {
|
|
173
|
+
const verified = await jwtVerify(match[1], config.jwks, {
|
|
174
|
+
issuer: config.issuer,
|
|
175
|
+
audience: config.audience
|
|
176
|
+
});
|
|
177
|
+
payload = verified.payload;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
status: 401,
|
|
182
|
+
error: "invalid_token",
|
|
183
|
+
description: error instanceof Error ? error.message : "Token verification failed"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (config.requiredScopes) {
|
|
187
|
+
const granted = tokenScopes(payload);
|
|
188
|
+
if (!config.requiredScopes.some((scope) => granted.includes(scope))) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
status: 403,
|
|
192
|
+
error: "insufficient_scope",
|
|
193
|
+
description: `Token carries none of the required scopes: ${config.requiredScopes.join(", ")}`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const caller = [payload.preferred_username, payload.upn, payload.appid].find((value) => typeof value === "string" && value.length > 0);
|
|
198
|
+
return { ok: true, subject: typeof payload.sub === "string" ? payload.sub : void 0, caller };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/oauth-proxy.ts
|
|
202
|
+
var AUTHORIZE_PATH = "/oauth/authorize";
|
|
203
|
+
var TOKEN_PATH = "/oauth/token";
|
|
204
|
+
var REGISTER_PATH = "/oauth/register";
|
|
205
|
+
var DEFAULT_REDIRECT_ALLOWLIST = [
|
|
206
|
+
"https://claude.ai/api/mcp/auth_callback",
|
|
207
|
+
"https://claude.com/api/mcp/auth_callback"
|
|
208
|
+
];
|
|
209
|
+
function isAllowedRedirect(uri, allowlist) {
|
|
210
|
+
if (allowlist.includes(uri)) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const parsed = new URL(uri);
|
|
215
|
+
return parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]");
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function getProxyConfig(auth, env = process.env) {
|
|
221
|
+
const configured = (env.MCP_OAUTH_REDIRECT_ALLOWLIST || "").split(/[\s,]+/).filter(Boolean);
|
|
222
|
+
return {
|
|
223
|
+
origin: serverOrigin(auth),
|
|
224
|
+
clientId: (env.MCP_OAUTH_CLIENT_ID || "").trim(),
|
|
225
|
+
upstreamScope: (env.MCP_OAUTH_UPSTREAM_SCOPE || "").trim() || [...auth.advertisedScopes ?? [], "offline_access"].join(" ").trim(),
|
|
226
|
+
redirectAllowlist: configured.length > 0 ? configured : DEFAULT_REDIRECT_ALLOWLIST
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function proxyAuthorizationServerMetadata(auth, proxy) {
|
|
230
|
+
return {
|
|
231
|
+
issuer: proxy.origin,
|
|
232
|
+
authorization_endpoint: `${proxy.origin}${AUTHORIZE_PATH}`,
|
|
233
|
+
token_endpoint: `${proxy.origin}${TOKEN_PATH}`,
|
|
234
|
+
response_types_supported: ["code"],
|
|
235
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
236
|
+
code_challenge_methods_supported: ["S256"],
|
|
237
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
238
|
+
...proxy.clientId ? { registration_endpoint: `${proxy.origin}${REGISTER_PATH}` } : {},
|
|
239
|
+
...auth.advertisedScopes ? { scopes_supported: auth.advertisedScopes } : {}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function buildAuthorizeRedirect(requestUrl, proxy, upstreamAuthorizeEndpoint) {
|
|
243
|
+
const redirectUri = requestUrl.searchParams.get("redirect_uri");
|
|
244
|
+
if (!redirectUri) {
|
|
245
|
+
return { status: 400, error: "invalid_request", description: "redirect_uri is required" };
|
|
246
|
+
}
|
|
247
|
+
if (!isAllowedRedirect(redirectUri, proxy.redirectAllowlist)) {
|
|
248
|
+
return {
|
|
249
|
+
status: 400,
|
|
250
|
+
error: "invalid_request",
|
|
251
|
+
description: "redirect_uri is not allowed for this server"
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const upstream = new URL(upstreamAuthorizeEndpoint);
|
|
255
|
+
const passthrough = [
|
|
256
|
+
"client_id",
|
|
257
|
+
"response_type",
|
|
258
|
+
"redirect_uri",
|
|
259
|
+
"state",
|
|
260
|
+
"code_challenge",
|
|
261
|
+
"code_challenge_method",
|
|
262
|
+
"prompt",
|
|
263
|
+
"login_hint"
|
|
264
|
+
];
|
|
265
|
+
for (const name of passthrough) {
|
|
266
|
+
const value = requestUrl.searchParams.get(name);
|
|
267
|
+
if (value !== null) {
|
|
268
|
+
upstream.searchParams.set(name, value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
upstream.searchParams.set("scope", proxy.upstreamScope);
|
|
272
|
+
return { status: 302, location: upstream.toString() };
|
|
273
|
+
}
|
|
274
|
+
function buildTokenRequestBody(rawBody) {
|
|
275
|
+
const params = new URLSearchParams(rawBody);
|
|
276
|
+
params.delete("resource");
|
|
277
|
+
return params;
|
|
278
|
+
}
|
|
279
|
+
async function forwardTokenRequest(rawBody, authorizationHeader, upstreamTokenEndpoint, fetchImpl = fetch) {
|
|
280
|
+
const response = await fetchImpl(upstreamTokenEndpoint, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
284
|
+
...authorizationHeader ? { Authorization: authorizationHeader } : {}
|
|
285
|
+
},
|
|
286
|
+
body: buildTokenRequestBody(rawBody).toString()
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
status: response.status,
|
|
290
|
+
body: await response.text(),
|
|
291
|
+
contentType: response.headers.get("content-type") ?? "application/json"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function resolveUpstreamEndpoints(auth, fetchImpl = fetch) {
|
|
295
|
+
const metadata = await authorizationServerMetadata(auth, fetchImpl);
|
|
296
|
+
const authorize = metadata.authorization_endpoint;
|
|
297
|
+
const token = metadata.token_endpoint;
|
|
298
|
+
if (typeof authorize !== "string" || typeof token !== "string") {
|
|
299
|
+
throw new Error("Issuer configuration is missing authorization_endpoint or token_endpoint");
|
|
300
|
+
}
|
|
301
|
+
return { authorize, token };
|
|
302
|
+
}
|
|
303
|
+
function redactUrlForLog(url) {
|
|
304
|
+
const [path, query] = url.split("?");
|
|
305
|
+
return query ? `${path}?<redacted>` : path;
|
|
306
|
+
}
|
|
307
|
+
function registerClient(request, proxy) {
|
|
308
|
+
if (!proxy.clientId) {
|
|
309
|
+
return {
|
|
310
|
+
status: 400,
|
|
311
|
+
error: "invalid_request",
|
|
312
|
+
description: "This server has no upstream client configured"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const body = request ?? {};
|
|
316
|
+
const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris : [];
|
|
317
|
+
if (redirectUris.length === 0) {
|
|
318
|
+
return { status: 400, error: "invalid_redirect_uri", description: "redirect_uris is required" };
|
|
319
|
+
}
|
|
320
|
+
const rejected = redirectUris.filter(
|
|
321
|
+
(uri) => typeof uri !== "string" || !isAllowedRedirect(uri, proxy.redirectAllowlist)
|
|
322
|
+
);
|
|
323
|
+
if (rejected.length > 0) {
|
|
324
|
+
return {
|
|
325
|
+
status: 400,
|
|
326
|
+
error: "invalid_redirect_uri",
|
|
327
|
+
description: "redirect_uris are not allowed for this server"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
status: 201,
|
|
332
|
+
body: {
|
|
333
|
+
client_id: proxy.clientId,
|
|
334
|
+
redirect_uris: redirectUris,
|
|
335
|
+
token_endpoint_auth_method: "none",
|
|
336
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
337
|
+
response_types: ["code"],
|
|
338
|
+
client_id_issued_at: Math.floor(Date.now() / 1e3),
|
|
339
|
+
...typeof body.client_name === "string" ? { client_name: body.client_name } : {}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
14
343
|
|
|
15
344
|
// src/settings.ts
|
|
16
345
|
import { z } from "zod";
|
|
17
346
|
var settingsSchema = z.object({
|
|
18
347
|
bitbucketWorkspace: z.string().min(1, "BITBUCKET_WORKSPACE is required"),
|
|
19
|
-
bitbucketEmail: z.string().
|
|
20
|
-
bitbucketApiToken: z.string().min(1, "BITBUCKET_API_TOKEN is required"),
|
|
348
|
+
bitbucketEmail: z.string().default(""),
|
|
349
|
+
bitbucketApiToken: z.string().min(1, "BITBUCKET_API_TOKEN (or BITBUCKET_OAUTH_TOKEN) is required"),
|
|
350
|
+
bitbucketAuthType: z.enum(["basic", "bearer"]),
|
|
21
351
|
apiTimeout: z.number().min(1).max(300).default(30),
|
|
22
352
|
maxRetries: z.number().min(0).max(10).default(3),
|
|
23
353
|
outputFormat: z.enum(["json", "toon"]).default("json")
|
|
24
|
-
})
|
|
354
|
+
}).refine(
|
|
355
|
+
(s) => s.bitbucketAuthType !== "basic" || s.bitbucketEmail.length > 0,
|
|
356
|
+
{ message: "BITBUCKET_EMAIL is required when using basic auth", path: ["bitbucketEmail"] }
|
|
357
|
+
);
|
|
25
358
|
var cachedSettings = null;
|
|
359
|
+
function resolveAuthType(explicit, oauthToken, email) {
|
|
360
|
+
const normalized = explicit.trim().toLowerCase();
|
|
361
|
+
if (normalized === "bearer" || normalized === "basic") {
|
|
362
|
+
return normalized;
|
|
363
|
+
}
|
|
364
|
+
if (oauthToken.length > 0 || email.length === 0) {
|
|
365
|
+
return "bearer";
|
|
366
|
+
}
|
|
367
|
+
return "basic";
|
|
368
|
+
}
|
|
26
369
|
function getSettings() {
|
|
27
370
|
if (cachedSettings) {
|
|
28
371
|
return cachedSettings;
|
|
29
372
|
}
|
|
373
|
+
const email = process.env.BITBUCKET_EMAIL || "";
|
|
374
|
+
const oauthToken = process.env.BITBUCKET_OAUTH_TOKEN || "";
|
|
375
|
+
const apiToken = process.env.BITBUCKET_API_TOKEN || "";
|
|
376
|
+
const authType = resolveAuthType(process.env.BITBUCKET_AUTH_TYPE || "", oauthToken, email);
|
|
30
377
|
const rawSettings = {
|
|
31
378
|
bitbucketWorkspace: process.env.BITBUCKET_WORKSPACE || "",
|
|
32
|
-
bitbucketEmail:
|
|
33
|
-
|
|
379
|
+
bitbucketEmail: email,
|
|
380
|
+
// Bearer mode prefers the dedicated OAuth/access token when both are set
|
|
381
|
+
bitbucketApiToken: authType === "bearer" ? oauthToken || apiToken : apiToken,
|
|
382
|
+
bitbucketAuthType: authType,
|
|
34
383
|
apiTimeout: parseInt(process.env.API_TIMEOUT || "30", 10),
|
|
35
384
|
maxRetries: parseInt(process.env.MAX_RETRIES || "3", 10),
|
|
36
385
|
outputFormat: process.env.OUTPUT_FORMAT || "json"
|
|
@@ -94,19 +443,26 @@ var BitbucketClient = class _BitbucketClient {
|
|
|
94
443
|
workspace;
|
|
95
444
|
client;
|
|
96
445
|
maxRetries;
|
|
446
|
+
requestTimeout;
|
|
97
447
|
constructor() {
|
|
98
448
|
const settings = getSettings();
|
|
99
449
|
this.workspace = settings.bitbucketWorkspace;
|
|
100
450
|
this.maxRetries = settings.maxRetries;
|
|
451
|
+
this.requestTimeout = settings.apiTimeout * 1e3;
|
|
452
|
+
const useBearer = settings.bitbucketAuthType === "bearer";
|
|
101
453
|
this.client = axios.create({
|
|
102
454
|
baseURL: _BitbucketClient.BASE_URL,
|
|
103
|
-
timeout:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
455
|
+
timeout: this.requestTimeout,
|
|
456
|
+
paramsSerializer: { indexes: null },
|
|
457
|
+
...useBearer ? {} : {
|
|
458
|
+
auth: {
|
|
459
|
+
username: settings.bitbucketEmail,
|
|
460
|
+
password: settings.bitbucketApiToken
|
|
461
|
+
}
|
|
107
462
|
},
|
|
108
463
|
headers: {
|
|
109
|
-
"Content-Type": "application/json"
|
|
464
|
+
"Content-Type": "application/json",
|
|
465
|
+
...useBearer ? { Authorization: `Bearer ${settings.bitbucketApiToken}` } : {}
|
|
110
466
|
}
|
|
111
467
|
});
|
|
112
468
|
}
|
|
@@ -117,103 +473,126 @@ var BitbucketClient = class _BitbucketClient {
|
|
|
117
473
|
const base = `repositories/${this.workspace}/${repoSlug}`;
|
|
118
474
|
return parts.length > 0 ? `${base}/${parts.join("/")}` : base;
|
|
119
475
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
476
|
+
encodeRef(ref) {
|
|
477
|
+
if (!ref || ref.split("/").some((part) => part === "." || part === "..")) throw new BitbucketError("Invalid revision name");
|
|
478
|
+
return encodeURIComponent(ref);
|
|
479
|
+
}
|
|
480
|
+
encodeFilePath(path) {
|
|
481
|
+
if (path.startsWith("/") || path.split("/").some((part) => part === "." || part === "..")) {
|
|
482
|
+
throw new BitbucketError("Invalid repository file path");
|
|
483
|
+
}
|
|
484
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
485
|
+
}
|
|
486
|
+
/** Retry only reads after ambiguous network/server failures; writes may retry 429. */
|
|
487
|
+
async request(method, path, data, params, text = false) {
|
|
124
488
|
let backoff = _BitbucketClient.INITIAL_BACKOFF;
|
|
489
|
+
let slept = 0;
|
|
490
|
+
const deadline = performance.now() + this.requestTimeout;
|
|
125
491
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
492
|
+
const remaining = deadline - performance.now();
|
|
493
|
+
if (remaining <= 0) throw new BitbucketError("Request retry budget exhausted", void 0, method, path);
|
|
126
494
|
try {
|
|
127
495
|
const response = await this.client.request({
|
|
128
496
|
method,
|
|
129
497
|
url: path,
|
|
130
498
|
data,
|
|
131
|
-
params
|
|
499
|
+
params,
|
|
500
|
+
timeout: Math.max(1, Math.floor(remaining)),
|
|
501
|
+
...text ? { responseType: "text", transformResponse: [(value) => value] } : {}
|
|
132
502
|
});
|
|
133
503
|
return response.data;
|
|
134
504
|
} catch (error) {
|
|
135
|
-
if (axios.isAxiosError(error))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
505
|
+
if (!axios.isAxiosError(error)) throw error;
|
|
506
|
+
const status = error.response?.status;
|
|
507
|
+
if (status === 404) return null;
|
|
508
|
+
const abortedBody = error.code === "ERR_BAD_RESPONSE" && error.message === "stream has been aborted";
|
|
509
|
+
const network = abortedBody || !error.response && ["ECONNRESET", "ECONNREFUSED", "ECONNABORTED", "ETIMEDOUT", "EPIPE", "EAI_AGAIN", "ENOTFOUND", "ERR_NETWORK"].includes(error.code || "");
|
|
510
|
+
const retryable = status === 429 || method === "GET" && (network || [502, 503, 504].includes(status ?? 0));
|
|
511
|
+
if (retryable && attempt < this.maxRetries) {
|
|
512
|
+
const header = error.response?.headers["retry-after"];
|
|
513
|
+
let wait = backoff;
|
|
514
|
+
if (typeof header === "string" && header.trim()) {
|
|
515
|
+
const numeric = Number(header);
|
|
516
|
+
const dated = /[a-z]/i.test(header) && !["nan", "inf", "infinity"].includes(header.toLowerCase()) ? Date.parse(header) - Date.now() : NaN;
|
|
517
|
+
const parsed = Number.isFinite(numeric) && numeric >= 0 ? numeric * 1e3 : dated;
|
|
518
|
+
if (Number.isFinite(parsed) && parsed >= 0) wait = parsed;
|
|
139
519
|
}
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
const retryAfter = axiosError.response.headers["retry-after"];
|
|
143
|
-
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : backoff;
|
|
144
|
-
await sleep(waitTime);
|
|
145
|
-
backoff *= 2;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
throw new BitbucketError(
|
|
149
|
-
`Rate limited after ${this.maxRetries} retries`,
|
|
150
|
-
429,
|
|
151
|
-
method,
|
|
152
|
-
path
|
|
153
|
-
);
|
|
520
|
+
if (wait > 15e3 - slept || wait >= deadline - performance.now()) {
|
|
521
|
+
throw new BitbucketError("Retry-After exceeds request retry budget; retry later", status, method, path);
|
|
154
522
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
statusCode,
|
|
160
|
-
method,
|
|
161
|
-
path
|
|
162
|
-
);
|
|
523
|
+
await sleep(wait);
|
|
524
|
+
slept += wait;
|
|
525
|
+
backoff *= 2;
|
|
526
|
+
continue;
|
|
163
527
|
}
|
|
164
|
-
throw
|
|
528
|
+
throw new BitbucketError(
|
|
529
|
+
network ? `Network request failed after ${attempt + 1} attempt(s)` : `API error ${status ?? "unknown"}`,
|
|
530
|
+
status,
|
|
531
|
+
method,
|
|
532
|
+
path
|
|
533
|
+
);
|
|
165
534
|
}
|
|
166
535
|
}
|
|
167
|
-
throw new BitbucketError(
|
|
536
|
+
throw new BitbucketError("Request retry budget exhausted", void 0, method, path);
|
|
168
537
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
538
|
+
requestText(path, params) {
|
|
539
|
+
return this.request("GET", path, void 0, params, true);
|
|
540
|
+
}
|
|
541
|
+
/** A next link can change only query parameters, never the authenticated endpoint. */
|
|
542
|
+
validateCursor(cursor, endpoint) {
|
|
543
|
+
const expected = new URL(endpoint, _BitbucketClient.BASE_URL + "/");
|
|
544
|
+
let parsed;
|
|
545
|
+
try {
|
|
546
|
+
parsed = new URL(cursor);
|
|
547
|
+
} catch {
|
|
548
|
+
throw new BitbucketError("Invalid pagination cursor");
|
|
549
|
+
}
|
|
550
|
+
const rawPath = cursor.split(/[?#]/, 1)[0].replace(/^https:\/\/[^/]+/, "");
|
|
551
|
+
const segments = rawPath.split("/");
|
|
552
|
+
const unsafeSegments = segments.some((segment) => {
|
|
553
|
+
let decoded;
|
|
175
554
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return response.data;
|
|
180
|
-
} catch (error) {
|
|
181
|
-
if (axios.isAxiosError(error)) {
|
|
182
|
-
if (error.response?.status === 404) {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
if (error.response?.status === 429) {
|
|
186
|
-
if (attempt < this.maxRetries) {
|
|
187
|
-
const retryAfter = error.response.headers["retry-after"];
|
|
188
|
-
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : backoff;
|
|
189
|
-
await sleep(waitTime);
|
|
190
|
-
backoff *= 2;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
throw new BitbucketError(`Request failed: ${error.response?.status}`);
|
|
195
|
-
}
|
|
196
|
-
throw error;
|
|
555
|
+
decoded = decodeURIComponent(segment);
|
|
556
|
+
} catch {
|
|
557
|
+
return true;
|
|
197
558
|
}
|
|
559
|
+
return decoded === "." || decoded === ".." || decoded.includes("\\");
|
|
560
|
+
});
|
|
561
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.bitbucket.org" || parsed.port && parsed.port !== "443" || parsed.username || parsed.password || parsed.hash || cursor.includes("\\") || unsafeSegments || parsed.pathname.replace(/\/$/, "") !== expected.pathname.replace(/\/$/, "")) {
|
|
562
|
+
throw new BitbucketError("Invalid pagination cursor for this endpoint");
|
|
198
563
|
}
|
|
199
|
-
return
|
|
564
|
+
return cursor;
|
|
565
|
+
}
|
|
566
|
+
async historyPage(endpoint, options = {}, allowMissing = false) {
|
|
567
|
+
const { limit = 50, cursor, maxPage = 100, ...filters } = options;
|
|
568
|
+
const params = { pagelen: Math.min(Math.max(1, Math.floor(limit)), maxPage), ...filters };
|
|
569
|
+
for (const key of Object.keys(params)) if (params[key] === void 0) delete params[key];
|
|
570
|
+
const result = await this.request(
|
|
571
|
+
"GET",
|
|
572
|
+
cursor ? this.validateCursor(cursor, endpoint) : endpoint,
|
|
573
|
+
void 0,
|
|
574
|
+
cursor ? void 0 : params
|
|
575
|
+
);
|
|
576
|
+
if (result === null && !allowMissing) {
|
|
577
|
+
throw new BitbucketError("History resource or revision not found (404)", 404, "GET", endpoint);
|
|
578
|
+
}
|
|
579
|
+
const next = result?.next ? this.validateCursor(result.next, endpoint) : null;
|
|
580
|
+
return { values: result?.values ?? [], has_more: next !== null, next_cursor: next };
|
|
200
581
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Helper for paginated list endpoints
|
|
203
|
-
*/
|
|
204
582
|
async paginatedList(endpoint, options = {}) {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
583
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 50));
|
|
584
|
+
const values = [];
|
|
585
|
+
const visited = /* @__PURE__ */ new Set();
|
|
586
|
+
let cursor;
|
|
587
|
+
for (let pageCount = 0; pageCount < 1e3; pageCount++) {
|
|
588
|
+
const page = await this.historyPage(endpoint, { ...options, cursor }, true);
|
|
589
|
+
values.push(...page.values);
|
|
590
|
+
if (values.length >= limit || !page.next_cursor) return values.slice(0, limit);
|
|
591
|
+
if (visited.has(page.next_cursor)) throw new BitbucketError("Repeated pagination cursor");
|
|
592
|
+
visited.add(page.next_cursor);
|
|
593
|
+
cursor = page.next_cursor;
|
|
594
|
+
}
|
|
595
|
+
throw new BitbucketError("Pagination page budget exhausted");
|
|
217
596
|
}
|
|
218
597
|
// ==================== REPOSITORIES ====================
|
|
219
598
|
async getRepository(repoSlug) {
|
|
@@ -582,21 +961,65 @@ var BitbucketClient = class _BitbucketClient {
|
|
|
582
961
|
});
|
|
583
962
|
}
|
|
584
963
|
async getBranch(repoSlug, branchName) {
|
|
585
|
-
return this.request("GET", this.repoPath(repoSlug, "refs", "branches", branchName));
|
|
964
|
+
return this.request("GET", this.repoPath(repoSlug, "refs", "branches", this.encodeRef(branchName)));
|
|
586
965
|
}
|
|
587
966
|
// ==================== COMMITS ====================
|
|
588
967
|
async listCommits(repoSlug, options = {}) {
|
|
589
968
|
return this.paginatedList(this.repoPath(repoSlug, "commits"), {
|
|
590
|
-
limit: options.limit
|
|
591
|
-
include: options.branch,
|
|
969
|
+
limit: options.limit ?? 20,
|
|
970
|
+
include: [...options.branch ? [options.branch] : [], ...options.include ?? []],
|
|
971
|
+
exclude: options.exclude,
|
|
972
|
+
path: options.path
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
listCommitsPage(repoSlug, options = {}) {
|
|
976
|
+
return this.historyPage(this.repoPath(repoSlug, "commits"), {
|
|
977
|
+
limit: options.limit ?? 20,
|
|
978
|
+
cursor: options.cursor,
|
|
979
|
+
include: [...options.branch ? [options.branch] : [], ...options.include ?? []],
|
|
980
|
+
exclude: options.exclude,
|
|
592
981
|
path: options.path
|
|
593
982
|
});
|
|
594
983
|
}
|
|
595
984
|
async getCommit(repoSlug, commit) {
|
|
596
|
-
return this.request("GET", this.repoPath(repoSlug, "commit", commit));
|
|
985
|
+
return this.request("GET", this.repoPath(repoSlug, "commit", this.encodeRef(commit)));
|
|
986
|
+
}
|
|
987
|
+
compareCommits(repoSlug, base, head) {
|
|
988
|
+
return this.request("GET", this.repoPath(repoSlug, "diffstat", this.revisionRange(base, head)), void 0, { topic: false });
|
|
989
|
+
}
|
|
990
|
+
async resolveComparisonRef(repoSlug, ref) {
|
|
991
|
+
this.encodeRef(ref);
|
|
992
|
+
const commit = await this.getCommit(repoSlug, ref);
|
|
993
|
+
if (!commit) throw new BitbucketError("Commit or revision not found (404)", 404, "GET");
|
|
994
|
+
if (!/^[a-fA-F0-9]{12,40}$/.test(commit.hash)) throw new BitbucketError("Invalid commit hash returned by Bitbucket");
|
|
995
|
+
return commit.hash.slice(0, 12).toLowerCase();
|
|
996
|
+
}
|
|
997
|
+
async compareCommitsPage(repoSlug, base, head, options = {}) {
|
|
998
|
+
const canonicalBase = await this.resolveComparisonRef(repoSlug, base);
|
|
999
|
+
const canonicalHead = await this.resolveComparisonRef(repoSlug, head);
|
|
1000
|
+
return this.historyPage(this.repoPath(repoSlug, "diffstat", this.revisionRange(canonicalBase, canonicalHead)), {
|
|
1001
|
+
limit: options.limit ?? 50,
|
|
1002
|
+
cursor: options.cursor,
|
|
1003
|
+
path: options.path,
|
|
1004
|
+
topic: false
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/** Bitbucket takes the source first, the opposite of git diff base head. */
|
|
1008
|
+
revisionRange(base, head) {
|
|
1009
|
+
return `${this.encodeRef(head)}..${this.encodeRef(base)}`;
|
|
597
1010
|
}
|
|
598
|
-
|
|
599
|
-
return this.
|
|
1011
|
+
getCommitDiff(repoSlug, base, head, options = {}) {
|
|
1012
|
+
return this.requestText(this.repoPath(repoSlug, "diff", this.revisionRange(base, head)), { topic: false, path: options.path });
|
|
1013
|
+
}
|
|
1014
|
+
getMergeBase(repoSlug, base, head) {
|
|
1015
|
+
return this.request("GET", this.repoPath(repoSlug, "merge-base", this.revisionRange(base, head)));
|
|
1016
|
+
}
|
|
1017
|
+
getFileHistory(repoSlug, path, ref = "main", options = {}) {
|
|
1018
|
+
if (!path) throw new BitbucketError("File history requires a file path");
|
|
1019
|
+
return this.historyPage(this.repoPath(repoSlug, "filehistory", this.encodeRef(ref), this.encodeFilePath(path)), {
|
|
1020
|
+
limit: options.limit ?? 20,
|
|
1021
|
+
cursor: options.cursor
|
|
1022
|
+
});
|
|
600
1023
|
}
|
|
601
1024
|
async getCommitStatuses(repoSlug, commit, options = {}) {
|
|
602
1025
|
return this.paginatedList(this.repoPath(repoSlug, "commit", commit, "statuses"), {
|
|
@@ -683,10 +1106,22 @@ var BitbucketClient = class _BitbucketClient {
|
|
|
683
1106
|
// ==================== TAGS ====================
|
|
684
1107
|
async listTags(repoSlug, options = {}) {
|
|
685
1108
|
return this.paginatedList(this.repoPath(repoSlug, "refs", "tags"), {
|
|
686
|
-
limit: options.limit
|
|
687
|
-
sort: "-target.date"
|
|
1109
|
+
limit: options.limit ?? 50,
|
|
1110
|
+
sort: options.sort ?? "-target.date",
|
|
1111
|
+
q: options.query
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
listTagsPage(repoSlug, options = {}) {
|
|
1115
|
+
return this.historyPage(this.repoPath(repoSlug, "refs", "tags"), {
|
|
1116
|
+
limit: options.limit ?? 50,
|
|
1117
|
+
cursor: options.cursor,
|
|
1118
|
+
sort: options.sort ?? "-target.date",
|
|
1119
|
+
q: options.query
|
|
688
1120
|
});
|
|
689
1121
|
}
|
|
1122
|
+
getTag(repoSlug, tagName) {
|
|
1123
|
+
return this.request("GET", this.repoPath(repoSlug, "refs", "tags", this.encodeRef(tagName)));
|
|
1124
|
+
}
|
|
690
1125
|
async createTag(repoSlug, name, target, message) {
|
|
691
1126
|
const payload = {
|
|
692
1127
|
name,
|
|
@@ -706,7 +1141,7 @@ var BitbucketClient = class _BitbucketClient {
|
|
|
706
1141
|
return result;
|
|
707
1142
|
}
|
|
708
1143
|
async deleteTag(repoSlug, tagName) {
|
|
709
|
-
await this.request("DELETE", this.repoPath(repoSlug, "refs", "tags", tagName));
|
|
1144
|
+
await this.request("DELETE", this.repoPath(repoSlug, "refs", "tags", this.encodeRef(tagName)));
|
|
710
1145
|
}
|
|
711
1146
|
// ==================== BRANCH RESTRICTIONS ====================
|
|
712
1147
|
async listBranchRestrictions(repoSlug, options = {}) {
|
|
@@ -746,10 +1181,10 @@ var BitbucketClient = class _BitbucketClient {
|
|
|
746
1181
|
}
|
|
747
1182
|
// ==================== SOURCE ====================
|
|
748
1183
|
async getFileContent(repoSlug, path, ref = "main") {
|
|
749
|
-
return this.requestText(this.repoPath(repoSlug, "src", ref, path));
|
|
1184
|
+
return this.requestText(this.repoPath(repoSlug, "src", this.encodeRef(ref), this.encodeFilePath(path)));
|
|
750
1185
|
}
|
|
751
1186
|
async listDirectory(repoSlug, path = "", options = {}) {
|
|
752
|
-
const endpoint = path ? this.repoPath(repoSlug, "src", options.ref || "main", path) : this.repoPath(repoSlug, "src", options.ref || "main");
|
|
1187
|
+
const endpoint = path ? this.repoPath(repoSlug, "src", this.encodeRef(options.ref || "main"), this.encodeFilePath(path)) : this.repoPath(repoSlug, "src", this.encodeRef(options.ref || "main"));
|
|
753
1188
|
return this.paginatedList(endpoint, { limit: options.limit || 100 });
|
|
754
1189
|
}
|
|
755
1190
|
// ==================== PERMISSIONS ====================
|
|
@@ -1733,12 +2168,15 @@ var handlers4 = {
|
|
|
1733
2168
|
var definitions5 = [
|
|
1734
2169
|
{
|
|
1735
2170
|
name: "list_commits",
|
|
1736
|
-
description: "List commits
|
|
2171
|
+
description: "List a page of commits. Use include/exclude for commits between revisions (include=head, exclude=base); continue with next_cursor until has_more is false.",
|
|
1737
2172
|
inputSchema: {
|
|
1738
2173
|
type: "object",
|
|
1739
2174
|
properties: {
|
|
1740
2175
|
repo_slug: { type: "string", description: "Repository slug" },
|
|
1741
|
-
branch: { type: "string", description: "
|
|
2176
|
+
branch: { type: "string", description: "Branch, tag or SHA to include (alias for include)" },
|
|
2177
|
+
include: { type: "array", items: { type: "string" }, description: "Include commits reachable from these branches, tags or SHAs" },
|
|
2178
|
+
exclude: { type: "array", items: { type: "string" }, description: "Exclude commits reachable from these revisions" },
|
|
2179
|
+
cursor: { type: "string", description: "Opaque next_cursor from this tool; retains its page size and filters" },
|
|
1742
2180
|
path: { type: "string", description: "Filter by file path - only commits that modified this path (optional)" },
|
|
1743
2181
|
limit: { type: "number", description: "Maximum results (default: 20)", default: 20 }
|
|
1744
2182
|
},
|
|
@@ -1759,17 +2197,45 @@ var definitions5 = [
|
|
|
1759
2197
|
},
|
|
1760
2198
|
{
|
|
1761
2199
|
name: "compare_commits",
|
|
1762
|
-
description: "Compare
|
|
2200
|
+
description: "Compare base to head (branches, tags or SHAs). Returns a page of changed files and line counts; follow next_cursor for all files.",
|
|
1763
2201
|
inputSchema: {
|
|
1764
2202
|
type: "object",
|
|
1765
2203
|
properties: {
|
|
1766
2204
|
repo_slug: { type: "string", description: "Repository slug" },
|
|
1767
2205
|
base: { type: "string", description: "Base commit hash or branch name" },
|
|
1768
|
-
head: { type: "string", description: "Head commit hash or branch name" }
|
|
2206
|
+
head: { type: "string", description: "Head commit hash, tag or branch name" },
|
|
2207
|
+
path: { type: "string", description: "Optional file path filter" },
|
|
2208
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
|
2209
|
+
cursor: { type: "string", description: "Opaque next_cursor; retains page size and filters" }
|
|
1769
2210
|
},
|
|
1770
2211
|
required: ["repo_slug", "base", "head"]
|
|
1771
2212
|
}
|
|
1772
2213
|
},
|
|
2214
|
+
{
|
|
2215
|
+
name: "get_commit_diff",
|
|
2216
|
+
description: "Get the raw Git-style diff from base to head. Continue large diffs with next_offset; pin revisions to commit hashes while paging. API-side diff limits still apply.",
|
|
2217
|
+
inputSchema: {
|
|
2218
|
+
type: "object",
|
|
2219
|
+
properties: {
|
|
2220
|
+
repo_slug: { type: "string" },
|
|
2221
|
+
base: { type: "string" },
|
|
2222
|
+
head: { type: "string" },
|
|
2223
|
+
path: { type: "string", description: "Optional file path filter" },
|
|
2224
|
+
offset: { type: "integer", minimum: 0, default: 0 },
|
|
2225
|
+
max_chars: { type: "integer", minimum: 1, maximum: 1e5, default: 2e4 }
|
|
2226
|
+
},
|
|
2227
|
+
required: ["repo_slug", "base", "head"]
|
|
2228
|
+
}
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
name: "get_merge_base",
|
|
2232
|
+
description: "Get a best common ancestor of two branches, tags or commit hashes. Bitbucket returns one ancestor when multiple best ancestors exist.",
|
|
2233
|
+
inputSchema: {
|
|
2234
|
+
type: "object",
|
|
2235
|
+
properties: { repo_slug: { type: "string" }, base: { type: "string" }, head: { type: "string" } },
|
|
2236
|
+
required: ["repo_slug", "base", "head"]
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
1773
2239
|
{
|
|
1774
2240
|
name: "get_commit_statuses",
|
|
1775
2241
|
description: "Get build/CI statuses for a commit.",
|
|
@@ -1804,18 +2270,18 @@ var definitions5 = [
|
|
|
1804
2270
|
var handlers5 = {
|
|
1805
2271
|
list_commits: async (args) => {
|
|
1806
2272
|
const client = getClient();
|
|
1807
|
-
const
|
|
2273
|
+
const page = await client.listCommitsPage(args.repo_slug, {
|
|
1808
2274
|
branch: args.branch,
|
|
2275
|
+
include: args.include,
|
|
2276
|
+
exclude: args.exclude,
|
|
1809
2277
|
path: args.path,
|
|
1810
|
-
|
|
2278
|
+
cursor: args.cursor,
|
|
2279
|
+
limit: validateLimit(args.limit ?? 20)
|
|
1811
2280
|
});
|
|
1812
2281
|
return {
|
|
1813
|
-
commits:
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
author: c.author?.raw,
|
|
1817
|
-
date: c.date
|
|
1818
|
-
}))
|
|
2282
|
+
commits: page.values.map((c) => ({ hash: truncateHash(c.hash), message: c.message, author: c.author?.raw, date: c.date })),
|
|
2283
|
+
has_more: page.has_more,
|
|
2284
|
+
next_cursor: page.next_cursor
|
|
1819
2285
|
};
|
|
1820
2286
|
},
|
|
1821
2287
|
get_commit: async (args) => {
|
|
@@ -1834,24 +2300,33 @@ var handlers5 = {
|
|
|
1834
2300
|
},
|
|
1835
2301
|
compare_commits: async (args) => {
|
|
1836
2302
|
const client = getClient();
|
|
1837
|
-
const
|
|
1838
|
-
args.
|
|
1839
|
-
args.
|
|
1840
|
-
args.
|
|
1841
|
-
);
|
|
1842
|
-
|
|
1843
|
-
return { error: `Could not compare ${args.base}..${args.head}` };
|
|
1844
|
-
}
|
|
1845
|
-
const files = result.values || [];
|
|
2303
|
+
const page = await client.compareCommitsPage(args.repo_slug, args.base, args.head, {
|
|
2304
|
+
path: args.path,
|
|
2305
|
+
limit: validateLimit(args.limit ?? 50),
|
|
2306
|
+
cursor: args.cursor
|
|
2307
|
+
});
|
|
2308
|
+
const files = page.values;
|
|
1846
2309
|
return {
|
|
1847
|
-
files: files.
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
"+": f.lines_added || 0,
|
|
1851
|
-
"-": f.lines_removed || 0
|
|
1852
|
-
}))
|
|
2310
|
+
files: files.map((f) => ({ path: f.new?.path || f.old?.path, status: f.status, "+": f.lines_added || 0, "-": f.lines_removed || 0 })),
|
|
2311
|
+
has_more: page.has_more,
|
|
2312
|
+
next_cursor: page.next_cursor
|
|
1853
2313
|
};
|
|
1854
2314
|
},
|
|
2315
|
+
get_commit_diff: async (args) => {
|
|
2316
|
+
const offset = args.offset === void 0 ? 0 : Number(args.offset);
|
|
2317
|
+
const maxChars = args.max_chars === void 0 ? 2e4 : Number(args.max_chars);
|
|
2318
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(maxChars) || maxChars < 1 || maxChars > 1e5) {
|
|
2319
|
+
return { success: false, error: "offset must be a nonnegative integer; max_chars must be an integer between 1 and 100000" };
|
|
2320
|
+
}
|
|
2321
|
+
const diff = await getClient().getCommitDiff(args.repo_slug, args.base, args.head, { path: args.path });
|
|
2322
|
+
if (diff === null) return notFoundResponse("Diff", `${args.base}..${args.head}`);
|
|
2323
|
+
const end = Math.min(offset + maxChars, diff.length);
|
|
2324
|
+
return { diff: diff.slice(offset, end), offset, next_offset: end < diff.length ? end : null, truncated: end < diff.length, total_chars: diff.length };
|
|
2325
|
+
},
|
|
2326
|
+
get_merge_base: async (args) => {
|
|
2327
|
+
const result = await getClient().getMergeBase(args.repo_slug, args.base, args.head);
|
|
2328
|
+
return result ? { hash: result.hash, message: result.message, author: result.author?.raw, date: result.date, parents: result.parents?.map((p) => truncateHash(p.hash)) } : notFoundResponse("Common ancestor", `${args.base}..${args.head}`);
|
|
2329
|
+
},
|
|
1855
2330
|
get_commit_statuses: async (args) => {
|
|
1856
2331
|
const client = getClient();
|
|
1857
2332
|
const statuses = await client.getCommitStatuses(args.repo_slug, args.commit, {
|
|
@@ -2099,16 +2574,24 @@ var handlers7 = {
|
|
|
2099
2574
|
var definitions8 = [
|
|
2100
2575
|
{
|
|
2101
2576
|
name: "list_tags",
|
|
2102
|
-
description: "List tags
|
|
2577
|
+
description: "List a page of tags, optionally filtered with Bitbucket query syntax and sorted (name uses natural version order). Follow next_cursor for all tags.",
|
|
2103
2578
|
inputSchema: {
|
|
2104
2579
|
type: "object",
|
|
2105
2580
|
properties: {
|
|
2106
2581
|
repo_slug: { type: "string", description: "Repository slug" },
|
|
2107
|
-
limit: { type: "
|
|
2582
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
|
2583
|
+
query: { type: "string", description: 'Bitbucket filter, e.g. name ~ "v2" or target.hash = "full-sha"' },
|
|
2584
|
+
sort: { type: "string", description: "Sort field, e.g. name, -name, -target.date", default: "-target.date" },
|
|
2585
|
+
cursor: { type: "string", description: "Opaque next_cursor; retains page size and filters" }
|
|
2108
2586
|
},
|
|
2109
2587
|
required: ["repo_slug"]
|
|
2110
2588
|
}
|
|
2111
2589
|
},
|
|
2590
|
+
{
|
|
2591
|
+
name: "get_tag",
|
|
2592
|
+
description: "Get a specific tag and its target commit without scanning tag pages.",
|
|
2593
|
+
inputSchema: { type: "object", properties: { repo_slug: { type: "string" }, tag_name: { type: "string" } }, required: ["repo_slug", "tag_name"] }
|
|
2594
|
+
},
|
|
2112
2595
|
{
|
|
2113
2596
|
name: "create_tag",
|
|
2114
2597
|
description: "Create a new tag in a repository.",
|
|
@@ -2139,19 +2622,28 @@ var definitions8 = [
|
|
|
2139
2622
|
var handlers8 = {
|
|
2140
2623
|
list_tags: async (args) => {
|
|
2141
2624
|
const client = getClient();
|
|
2142
|
-
const
|
|
2143
|
-
limit: validateLimit(args.limit
|
|
2625
|
+
const page = await client.listTagsPage(args.repo_slug, {
|
|
2626
|
+
limit: validateLimit(args.limit ?? 50),
|
|
2627
|
+
query: args.query,
|
|
2628
|
+
sort: args.sort,
|
|
2629
|
+
cursor: args.cursor
|
|
2144
2630
|
});
|
|
2145
2631
|
return {
|
|
2146
|
-
tags:
|
|
2632
|
+
tags: page.values.map((t) => ({
|
|
2147
2633
|
name: t.name,
|
|
2148
2634
|
target: truncateHash(t.target?.hash),
|
|
2149
2635
|
message: t.message,
|
|
2150
2636
|
date: t.target?.date,
|
|
2151
2637
|
tagger: t.tagger?.raw
|
|
2152
|
-
}))
|
|
2638
|
+
})),
|
|
2639
|
+
has_more: page.has_more,
|
|
2640
|
+
next_cursor: page.next_cursor
|
|
2153
2641
|
};
|
|
2154
2642
|
},
|
|
2643
|
+
get_tag: async (args) => {
|
|
2644
|
+
const tag = await getClient().getTag(args.repo_slug, args.tag_name);
|
|
2645
|
+
return tag ? { name: tag.name, target: tag.target?.hash, message: tag.message, date: tag.date, tagger: tag.tagger?.raw } : notFoundResponse("Tag", args.tag_name);
|
|
2646
|
+
},
|
|
2155
2647
|
create_tag: async (args) => {
|
|
2156
2648
|
const client = getClient();
|
|
2157
2649
|
const result = await client.createTag(
|
|
@@ -2272,6 +2764,21 @@ var definitions10 = [
|
|
|
2272
2764
|
required: ["repo_slug", "path"]
|
|
2273
2765
|
}
|
|
2274
2766
|
},
|
|
2767
|
+
{
|
|
2768
|
+
name: "get_file_history",
|
|
2769
|
+
description: "List file-history paths and full commit hashes at a branch, tag or SHA. Use get_commit for author, message and date. Follow next_cursor for older changes.",
|
|
2770
|
+
inputSchema: {
|
|
2771
|
+
type: "object",
|
|
2772
|
+
properties: {
|
|
2773
|
+
repo_slug: { type: "string" },
|
|
2774
|
+
path: { type: "string" },
|
|
2775
|
+
ref: { type: "string", default: "main" },
|
|
2776
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
2777
|
+
cursor: { type: "string", description: "Opaque next_cursor; retains page size and filters" }
|
|
2778
|
+
},
|
|
2779
|
+
required: ["repo_slug", "path"]
|
|
2780
|
+
}
|
|
2781
|
+
},
|
|
2275
2782
|
{
|
|
2276
2783
|
name: "list_directory",
|
|
2277
2784
|
description: "List contents of a directory in a repository. Browse repository structure without cloning.",
|
|
@@ -2302,6 +2809,17 @@ var handlers10 = {
|
|
|
2302
2809
|
size: content.length
|
|
2303
2810
|
};
|
|
2304
2811
|
},
|
|
2812
|
+
get_file_history: async (args) => {
|
|
2813
|
+
const page = await getClient().getFileHistory(args.repo_slug, args.path, args.ref || "main", {
|
|
2814
|
+
limit: validateLimit(args.limit ?? 20),
|
|
2815
|
+
cursor: args.cursor
|
|
2816
|
+
});
|
|
2817
|
+
return {
|
|
2818
|
+
history: page.values.map((entry) => ({ path: entry.path, commit: { hash: entry.commit.hash } })),
|
|
2819
|
+
has_more: page.has_more,
|
|
2820
|
+
next_cursor: page.next_cursor
|
|
2821
|
+
};
|
|
2822
|
+
},
|
|
2305
2823
|
list_directory: async (args) => {
|
|
2306
2824
|
const client = getClient();
|
|
2307
2825
|
const ref = args.ref || "main";
|
|
@@ -2941,7 +3459,7 @@ Summarize:
|
|
|
2941
3459
|
}
|
|
2942
3460
|
|
|
2943
3461
|
// src/index.ts
|
|
2944
|
-
var VERSION = "0.
|
|
3462
|
+
var VERSION = "0.19.0";
|
|
2945
3463
|
function createServer() {
|
|
2946
3464
|
const server = new Server(
|
|
2947
3465
|
{
|
|
@@ -3026,6 +3544,180 @@ function createServer() {
|
|
|
3026
3544
|
});
|
|
3027
3545
|
return server;
|
|
3028
3546
|
}
|
|
3547
|
+
async function startStdio() {
|
|
3548
|
+
const server = createServer();
|
|
3549
|
+
const transport = new StdioServerTransport();
|
|
3550
|
+
await server.connect(transport);
|
|
3551
|
+
console.error(`Bitbucket MCP Server v${VERSION} started (stdio)`);
|
|
3552
|
+
}
|
|
3553
|
+
function createHttpHandler() {
|
|
3554
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
3555
|
+
const auth = getAuthConfig();
|
|
3556
|
+
const proxy = auth ? getProxyConfig(auth) : null;
|
|
3557
|
+
return async (req, res) => {
|
|
3558
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
3559
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE");
|
|
3560
|
+
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, mcp-session-id");
|
|
3561
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
3562
|
+
if (req.method === "OPTIONS") {
|
|
3563
|
+
res.writeHead(204);
|
|
3564
|
+
res.end();
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? req.socket.remoteAddress ?? "unknown";
|
|
3568
|
+
res.on("finish", () => {
|
|
3569
|
+
console.error(
|
|
3570
|
+
`${req.method} ${redactUrlForLog(req.url ?? "")} -> ${res.statusCode} from ${clientIp}${callerLabel ? ` (${callerLabel})` : ""}`
|
|
3571
|
+
);
|
|
3572
|
+
});
|
|
3573
|
+
let callerLabel = "";
|
|
3574
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
3575
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3576
|
+
res.end(JSON.stringify({ status: "ok", version: VERSION }));
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
if (auth && req.method === "GET" && METADATA_PATHS.includes(req.url || "")) {
|
|
3580
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3581
|
+
res.end(JSON.stringify(protectedResourceMetadata(auth)));
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
if (auth && proxy && req.method === "GET" && req.url === AS_METADATA_PATH) {
|
|
3585
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3586
|
+
res.end(JSON.stringify(proxyAuthorizationServerMetadata(auth, proxy)));
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
if (auth && proxy && req.method === "GET" && req.url?.startsWith(AUTHORIZE_PATH)) {
|
|
3590
|
+
try {
|
|
3591
|
+
const { authorize } = await resolveUpstreamEndpoints(auth);
|
|
3592
|
+
const requestUrl = new URL(req.url, proxy.origin);
|
|
3593
|
+
const result = buildAuthorizeRedirect(requestUrl, proxy, authorize);
|
|
3594
|
+
if (result.status === 302) {
|
|
3595
|
+
res.writeHead(302, { Location: result.location });
|
|
3596
|
+
res.end();
|
|
3597
|
+
} else {
|
|
3598
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3599
|
+
res.end(JSON.stringify({ error: result.error, error_description: result.description }));
|
|
3600
|
+
}
|
|
3601
|
+
} catch (error) {
|
|
3602
|
+
console.error(`Authorization request could not be forwarded: ${error}`);
|
|
3603
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
3604
|
+
res.end(JSON.stringify({ error: "authorization_server_unavailable" }));
|
|
3605
|
+
}
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
if (auth && proxy && req.method === "POST" && req.url === REGISTER_PATH) {
|
|
3609
|
+
let parsed = {};
|
|
3610
|
+
try {
|
|
3611
|
+
parsed = JSON.parse((await readHttpBody(req.iterator({ destroyOnReturn: false }))).toString("utf8") || "{}");
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
const status = error instanceof HttpInputError ? error.status : 400;
|
|
3614
|
+
res.writeHead(status, { "Content-Type": "application/json", Connection: "close" });
|
|
3615
|
+
res.end(JSON.stringify({ error: "invalid_request", error_description: error instanceof HttpInputError ? error.message : "Body must be JSON" }));
|
|
3616
|
+
return;
|
|
3617
|
+
}
|
|
3618
|
+
const result = registerClient(parsed, proxy);
|
|
3619
|
+
if (result.status === 201) {
|
|
3620
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
3621
|
+
res.end(JSON.stringify(result.body));
|
|
3622
|
+
} else {
|
|
3623
|
+
res.writeHead(result.status, { "Content-Type": "application/json" });
|
|
3624
|
+
res.end(JSON.stringify({ error: result.error, error_description: result.description }));
|
|
3625
|
+
}
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
if (auth && proxy && req.method === "POST" && req.url === TOKEN_PATH) {
|
|
3629
|
+
try {
|
|
3630
|
+
const body = await readHttpBody(req.iterator({ destroyOnReturn: false }));
|
|
3631
|
+
const { token } = await resolveUpstreamEndpoints(auth);
|
|
3632
|
+
const upstream = await forwardTokenRequest(
|
|
3633
|
+
body.toString("utf8"),
|
|
3634
|
+
req.headers.authorization,
|
|
3635
|
+
token
|
|
3636
|
+
);
|
|
3637
|
+
res.writeHead(upstream.status, { "Content-Type": upstream.contentType });
|
|
3638
|
+
res.end(upstream.body);
|
|
3639
|
+
} catch (error) {
|
|
3640
|
+
if (error instanceof HttpInputError) {
|
|
3641
|
+
res.writeHead(error.status, { "Content-Type": "application/json", Connection: "close" });
|
|
3642
|
+
res.end(JSON.stringify({ error: "invalid_request", error_description: error.message }));
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
console.error(`Token request could not be forwarded: ${error}`);
|
|
3646
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
3647
|
+
res.end(JSON.stringify({ error: "authorization_server_unavailable" }));
|
|
3648
|
+
}
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
if (req.url !== "/mcp") {
|
|
3652
|
+
res.writeHead(404);
|
|
3653
|
+
res.end("Not found");
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
if (auth) {
|
|
3657
|
+
const result = await verifyBearer(req.headers.authorization, auth);
|
|
3658
|
+
if (result.ok) {
|
|
3659
|
+
callerLabel = result.caller ?? result.subject ?? "authenticated";
|
|
3660
|
+
}
|
|
3661
|
+
if (!result.ok) {
|
|
3662
|
+
res.writeHead(result.status, {
|
|
3663
|
+
"Content-Type": "application/json",
|
|
3664
|
+
"WWW-Authenticate": wwwAuthenticate(auth, result)
|
|
3665
|
+
});
|
|
3666
|
+
res.end(JSON.stringify({ error: result.error, error_description: result.description }));
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
let parsedBody;
|
|
3671
|
+
if (req.method === "POST") {
|
|
3672
|
+
try {
|
|
3673
|
+
parsedBody = parseMcpBody(await readHttpBody(req.iterator({ destroyOnReturn: false })));
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
const status = error instanceof HttpInputError ? error.status : 400;
|
|
3676
|
+
res.writeHead(status, { "Content-Type": "application/json", Connection: "close" });
|
|
3677
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", id: null, error: {
|
|
3678
|
+
code: -32600,
|
|
3679
|
+
message: error instanceof HttpInputError ? error.message : "Invalid JSON"
|
|
3680
|
+
} }));
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3685
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
3686
|
+
const session = sessions.get(sessionId);
|
|
3687
|
+
await session.transport.handleRequest(req, res, parsedBody);
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (req.method === "POST" && !sessionId) {
|
|
3691
|
+
const transport = new StreamableHTTPServerTransport({
|
|
3692
|
+
sessionIdGenerator: () => crypto.randomUUID()
|
|
3693
|
+
});
|
|
3694
|
+
const server = createServer();
|
|
3695
|
+
transport.onclose = () => {
|
|
3696
|
+
const sid = transport._sessionId;
|
|
3697
|
+
if (sid) sessions.delete(sid);
|
|
3698
|
+
server.close();
|
|
3699
|
+
};
|
|
3700
|
+
await server.connect(transport);
|
|
3701
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
3702
|
+
const newSessionId = res.getHeader("mcp-session-id");
|
|
3703
|
+
if (newSessionId) {
|
|
3704
|
+
sessions.set(newSessionId, { server, transport });
|
|
3705
|
+
}
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3709
|
+
res.end(JSON.stringify({ error: "Invalid or missing session" }));
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
async function startHttp() {
|
|
3713
|
+
const port = parseInt(process.env.PORT || "3000", 10);
|
|
3714
|
+
const auth = getAuthConfig();
|
|
3715
|
+
const httpServer = createHttpServer(createHttpHandler());
|
|
3716
|
+
httpServer.listen(port, () => {
|
|
3717
|
+
const mode = auth ? `OAuth required (issuer ${auth.issuer})` : "unauthenticated";
|
|
3718
|
+
console.error(`Bitbucket MCP Server v${VERSION} started (http) on port ${port} \u2014 ${mode}`);
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3029
3721
|
async function main() {
|
|
3030
3722
|
try {
|
|
3031
3723
|
getSettings();
|
|
@@ -3033,12 +3725,19 @@ async function main() {
|
|
|
3033
3725
|
console.error("Configuration error:", error instanceof Error ? error.message : error);
|
|
3034
3726
|
process.exit(1);
|
|
3035
3727
|
}
|
|
3036
|
-
const
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3728
|
+
const transport = process.env.MCP_TRANSPORT || (process.argv.includes("--http") ? "http" : "stdio");
|
|
3729
|
+
if (transport === "http") {
|
|
3730
|
+
await startHttp();
|
|
3731
|
+
} else {
|
|
3732
|
+
await startStdio();
|
|
3733
|
+
}
|
|
3040
3734
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3735
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
3736
|
+
main().catch((error) => {
|
|
3737
|
+
console.error("Fatal error:", error);
|
|
3738
|
+
process.exit(1);
|
|
3739
|
+
});
|
|
3740
|
+
}
|
|
3741
|
+
export {
|
|
3742
|
+
createHttpHandler
|
|
3743
|
+
};
|