@leadbay/mcp 0.20.1 → 0.21.1
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/CHANGELOG.md +9 -0
- package/README.md +9 -7
- package/dist/bin.js +38 -8
- package/dist/http-server.js +154 -32
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.21.1 — 2026-06-16
|
|
4
|
+
|
|
5
|
+
- **CSV import no longer 400s on a lead status the agent didn't uppercase** (product#3745): `leadbay_import_leads` / `leadbay_import_and_qualify` forwarded `default_status` / `statuses` values verbatim to `POST /imports/{id}/update_mappings`, whose backend `MappingsPayload` decodes them as the strict, case-sensitive `LeadStatus` enum (`DEFAULT, INBOUND, UNWANTED, WANTED, LOST, WON`). A value like "Won" failed deserialization and the whole call 400'd with an opaque "JSON deserialization error" before any record committed — JM hit this trying to tag 179 companies as Won. The MCP now owns the canonical set and enforces it before sending: status values are matched case-insensitively to their enum member ("Won" → "WON"), an empty default means no default, and a genuinely unknown status returns a clear `IMPORT_INVALID_STATUS` error naming the valid values instead of an opaque backend 400. The two tools' input schemas now declare the enum.
|
|
6
|
+
|
|
7
|
+
## 0.21.0 — 2026-06-16
|
|
8
|
+
|
|
9
|
+
- **Hosted MCP now triggers OAuth sign-in in Claude Desktop / ChatGPT** (remote custom connectors): the Fly endpoint was not an OAuth-compliant resource server, so a remote client had nothing to discover, never prompted the user to sign in, and then surfaced a host-side "needs auth / token expired" state even though the user never had a token. The server now implements the MCP authorization spec (RFC 9728): it serves OAuth 2.0 Protected Resource Metadata at `/.well-known/oauth-protected-resource[/<resource>]` and answers an unauthenticated (or invalid/expired) `POST /mcp` with `401` + `WWW-Authenticate: Bearer ... resource_metadata="…"`. The client discovers the Leadbay authorization server (the existing regional backend used by `login --oauth`) and runs the browser sign-in. Tool requests auto-probe both regions, so a valid token routes correctly and a stale one re-prompts instead of erroring.
|
|
10
|
+
- **Region-pinned connector URLs**: OAuth discovery runs before sign-in and Leadbay tokens are region-scoped, so the region is encoded in the URL. US accounts use `https://leadbay-mcp-prod.fly.dev/mcp`; FR accounts use `https://leadbay-mcp-prod.fly.dev/fr/mcp`. The path only selects which authorization server the sign-in prompt points at. Permissive CORS + an `OPTIONS` preflight are served on the discovery and MCP endpoints for browser-based remote clients. README's remote-client section updated to document Claude Desktop and the per-region URLs.
|
|
11
|
+
|
|
3
12
|
## 0.20.1 — 2026-06-15
|
|
4
13
|
|
|
5
14
|
- **Triage board stays the first next-step option on a poor-fit batch** (`leadbay_daily_check_in`): when today's batch is an ICP mismatch (every lead AI-scored off-profile), the agent was demoting the interactive triage board below "refine audience" in the NEXT STEPS widget — the plain ordering rule kept losing to the agent's own leverage judgment ("the whole batch is junk, so lead with fixing the lens"). The workflow contract requires the named artifact to be the FIRST option. The ordering rule now holds the triage board at position 1 even on a mismatched batch; the mismatch is surfaced in the prose nudge and offered as a *later* "refine the lens" option, never by displacing the artifact. Verified 5/5/5/5 across 3 consecutive eval runs on an all-off-ICP batch (the exact case that defeated the weaker rule).
|
package/README.md
CHANGED
|
@@ -290,21 +290,23 @@ Leadbay connection OK.
|
|
|
290
290
|
AI credits: 420 / 1000
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
###
|
|
293
|
+
### Claude Desktop / ChatGPT / remote-MCP clients
|
|
294
294
|
|
|
295
|
-
Leadbay runs a hosted MCP server that any remote-MCP client can connect to without a local install:
|
|
295
|
+
Leadbay runs a hosted MCP server that any remote-MCP client can connect to without a local install. Pick the URL for your account's region:
|
|
296
296
|
|
|
297
297
|
```
|
|
298
|
-
https://leadbay-mcp-prod.fly.dev/mcp
|
|
298
|
+
https://leadbay-mcp-prod.fly.dev/mcp # US accounts
|
|
299
|
+
https://leadbay-mcp-prod.fly.dev/fr/mcp # FR accounts
|
|
299
300
|
```
|
|
300
301
|
|
|
301
|
-
**
|
|
302
|
+
- **Claude Desktop**: Settings → Connectors → Add custom connector → paste the URL.
|
|
303
|
+
- **ChatGPT Desktop**: Settings → Apps → Add app → paste the URL.
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
On first connect the client runs the Leadbay OAuth sign-in (the server advertises OAuth 2.0 Protected Resource Metadata per RFC 9728 and challenges unauthenticated requests with `401 + WWW-Authenticate`). Sign in once in the browser; the client stores the token and sends it as `Authorization: Bearer <token>` on every request. No token to copy-paste, no local Node install needed.
|
|
304
306
|
|
|
305
|
-
|
|
307
|
+
The region is encoded in the URL because OAuth discovery happens before sign-in and Leadbay tokens are region-scoped — a US account uses `/mcp`, a FR account uses `/fr/mcp`. If the sign-in prompt never appears, you're on an old build of the hosted server (pre-0.21.0); it auto-updates on release.
|
|
306
308
|
|
|
307
|
-
|
|
309
|
+
**Updates are automatic** — the hosted server is always running the latest published release. You never need to update a config file or restart anything on your side.
|
|
308
310
|
|
|
309
311
|
## 4. Example prompts that work
|
|
310
312
|
|
package/dist/bin.js
CHANGED
|
@@ -10828,6 +10828,13 @@ function coerceCell(client, v, path) {
|
|
|
10828
10828
|
return String(v);
|
|
10829
10829
|
throw client.makeError("IMPORT_INVALID_CELL_TYPE", `Cell at ${path} is ${Array.isArray(v) ? "an array" : typeof v}, expected string|number|boolean|null`, `Convert the value to a string before passing.`, "POST /imports");
|
|
10830
10830
|
}
|
|
10831
|
+
function enforceLeadStatus(client, raw, path) {
|
|
10832
|
+
const canonical = String(raw).trim().toUpperCase();
|
|
10833
|
+
if (!LEAD_STATUS_SET.has(canonical)) {
|
|
10834
|
+
throw client.makeError("IMPORT_INVALID_STATUS", `${path} ${JSON.stringify(raw)} is not a valid lead status`, `Use one of ${LEAD_STATUSES.join(", ")} (case-insensitive), or omit it.`, "POST /imports");
|
|
10835
|
+
}
|
|
10836
|
+
return canonical;
|
|
10837
|
+
}
|
|
10831
10838
|
function prepareDomainsMode(client, inputs) {
|
|
10832
10839
|
const validInputs = [];
|
|
10833
10840
|
const malformedDomains = [];
|
|
@@ -10949,6 +10956,12 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
10949
10956
|
if (normDomain && !byDomain.has(normDomain))
|
|
10950
10957
|
byDomain.set(normDomain, idx);
|
|
10951
10958
|
});
|
|
10959
|
+
const statuses = {};
|
|
10960
|
+
for (const [cell, status] of Object.entries(mappings.statuses ?? {})) {
|
|
10961
|
+
statuses[cell] = enforceLeadStatus(client, status, `mappings.statuses[${JSON.stringify(cell)}]`);
|
|
10962
|
+
}
|
|
10963
|
+
const rawDefault = mappings.default_status;
|
|
10964
|
+
const default_status = rawDefault == null || String(rawDefault).trim() === "" ? null : enforceLeadStatus(client, rawDefault, "mappings.default_status");
|
|
10952
10965
|
return {
|
|
10953
10966
|
mode: "records",
|
|
10954
10967
|
validInputs,
|
|
@@ -10958,8 +10971,8 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
10958
10971
|
header,
|
|
10959
10972
|
mappings: {
|
|
10960
10973
|
fields: { ...normalizedFields },
|
|
10961
|
-
statuses
|
|
10962
|
-
default_status
|
|
10974
|
+
statuses,
|
|
10975
|
+
default_status
|
|
10963
10976
|
}
|
|
10964
10977
|
};
|
|
10965
10978
|
}
|
|
@@ -11334,7 +11347,7 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
11334
11347
|
})();
|
|
11335
11348
|
}, 0);
|
|
11336
11349
|
}
|
|
11337
|
-
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, PUBLIC_MAILBOX_DOMAINS, importLeads;
|
|
11350
|
+
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, PUBLIC_MAILBOX_DOMAINS, LEAD_STATUSES, LEAD_STATUS_SET, importLeads;
|
|
11338
11351
|
var init_import_leads = __esm({
|
|
11339
11352
|
"../core/dist/composite/import-leads.js"() {
|
|
11340
11353
|
"use strict";
|
|
@@ -11379,6 +11392,15 @@ var init_import_leads = __esm({
|
|
|
11379
11392
|
"163.com",
|
|
11380
11393
|
"126.com"
|
|
11381
11394
|
]);
|
|
11395
|
+
LEAD_STATUSES = [
|
|
11396
|
+
"DEFAULT",
|
|
11397
|
+
"INBOUND",
|
|
11398
|
+
"UNWANTED",
|
|
11399
|
+
"WANTED",
|
|
11400
|
+
"LOST",
|
|
11401
|
+
"WON"
|
|
11402
|
+
];
|
|
11403
|
+
LEAD_STATUS_SET = new Set(LEAD_STATUSES);
|
|
11382
11404
|
importLeads = {
|
|
11383
11405
|
name: "leadbay_import_leads",
|
|
11384
11406
|
annotations: {
|
|
@@ -11437,11 +11459,12 @@ var init_import_leads = __esm({
|
|
|
11437
11459
|
},
|
|
11438
11460
|
statuses: {
|
|
11439
11461
|
type: "object",
|
|
11440
|
-
description:
|
|
11462
|
+
description: `Optional map of raw CSV status-cell text \u2192 lead status (rarely needed). Keys are the verbatim cell strings; values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to {}.`
|
|
11441
11463
|
},
|
|
11442
11464
|
default_status: {
|
|
11443
11465
|
type: ["string", "null"],
|
|
11444
|
-
|
|
11466
|
+
enum: [...LEAD_STATUSES, null],
|
|
11467
|
+
description: `Optional default lead status applied to rows without an explicit status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to null.`
|
|
11445
11468
|
}
|
|
11446
11469
|
},
|
|
11447
11470
|
required: ["fields"]
|
|
@@ -17499,8 +17522,15 @@ var init_import_and_qualify = __esm({
|
|
|
17499
17522
|
type: "object",
|
|
17500
17523
|
description: "Ergonomic shorthand: `{CsvColumn: <number-id>}` or `{CsvColumn: '<field-name>'}` for custom-field mappings. Resolved against /crm/custom_fields catalog."
|
|
17501
17524
|
},
|
|
17502
|
-
statuses: {
|
|
17503
|
-
|
|
17525
|
+
statuses: {
|
|
17526
|
+
type: "object",
|
|
17527
|
+
description: `Optional map of raw CSV status-cell text \u2192 lead status. Values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive); keys are the verbatim cell strings.`
|
|
17528
|
+
},
|
|
17529
|
+
default_status: {
|
|
17530
|
+
type: ["string", "null"],
|
|
17531
|
+
enum: [...LEAD_STATUSES, null],
|
|
17532
|
+
description: `Optional default lead status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive).`
|
|
17533
|
+
}
|
|
17504
17534
|
},
|
|
17505
17535
|
// mappings has a closed shape (fields/custom_fields/statuses/default_status).
|
|
17506
17536
|
// Inner objects (fields, custom_fields, statuses) keep open shapes
|
|
@@ -25928,7 +25958,7 @@ var OAUTH_BASE_URLS = {
|
|
|
25928
25958
|
fr: "https://staging.api.leadbay.app"
|
|
25929
25959
|
}
|
|
25930
25960
|
};
|
|
25931
|
-
var VERSION = "0.
|
|
25961
|
+
var VERSION = "0.21.1";
|
|
25932
25962
|
var HELP = `
|
|
25933
25963
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
25934
25964
|
|
package/dist/http-server.js
CHANGED
|
@@ -7,6 +7,9 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/http-server.ts
|
|
9
9
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
10
|
+
import { realpathSync } from "fs";
|
|
11
|
+
import { basename } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
10
13
|
import { Hono } from "hono";
|
|
11
14
|
import { bodyLimit } from "hono/body-limit";
|
|
12
15
|
import { serve } from "@hono/node-server";
|
|
@@ -11521,6 +11524,22 @@ function coerceCell(client, v, path) {
|
|
|
11521
11524
|
return String(v);
|
|
11522
11525
|
throw client.makeError("IMPORT_INVALID_CELL_TYPE", `Cell at ${path} is ${Array.isArray(v) ? "an array" : typeof v}, expected string|number|boolean|null`, `Convert the value to a string before passing.`, "POST /imports");
|
|
11523
11526
|
}
|
|
11527
|
+
var LEAD_STATUSES = [
|
|
11528
|
+
"DEFAULT",
|
|
11529
|
+
"INBOUND",
|
|
11530
|
+
"UNWANTED",
|
|
11531
|
+
"WANTED",
|
|
11532
|
+
"LOST",
|
|
11533
|
+
"WON"
|
|
11534
|
+
];
|
|
11535
|
+
var LEAD_STATUS_SET = new Set(LEAD_STATUSES);
|
|
11536
|
+
function enforceLeadStatus(client, raw, path) {
|
|
11537
|
+
const canonical = String(raw).trim().toUpperCase();
|
|
11538
|
+
if (!LEAD_STATUS_SET.has(canonical)) {
|
|
11539
|
+
throw client.makeError("IMPORT_INVALID_STATUS", `${path} ${JSON.stringify(raw)} is not a valid lead status`, `Use one of ${LEAD_STATUSES.join(", ")} (case-insensitive), or omit it.`, "POST /imports");
|
|
11540
|
+
}
|
|
11541
|
+
return canonical;
|
|
11542
|
+
}
|
|
11524
11543
|
function prepareDomainsMode(client, inputs) {
|
|
11525
11544
|
const validInputs = [];
|
|
11526
11545
|
const malformedDomains = [];
|
|
@@ -11642,6 +11661,12 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
11642
11661
|
if (normDomain && !byDomain.has(normDomain))
|
|
11643
11662
|
byDomain.set(normDomain, idx);
|
|
11644
11663
|
});
|
|
11664
|
+
const statuses = {};
|
|
11665
|
+
for (const [cell, status] of Object.entries(mappings.statuses ?? {})) {
|
|
11666
|
+
statuses[cell] = enforceLeadStatus(client, status, `mappings.statuses[${JSON.stringify(cell)}]`);
|
|
11667
|
+
}
|
|
11668
|
+
const rawDefault = mappings.default_status;
|
|
11669
|
+
const default_status = rawDefault == null || String(rawDefault).trim() === "" ? null : enforceLeadStatus(client, rawDefault, "mappings.default_status");
|
|
11645
11670
|
return {
|
|
11646
11671
|
mode: "records",
|
|
11647
11672
|
validInputs,
|
|
@@ -11651,8 +11676,8 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
11651
11676
|
header,
|
|
11652
11677
|
mappings: {
|
|
11653
11678
|
fields: { ...normalizedFields },
|
|
11654
|
-
statuses
|
|
11655
|
-
default_status
|
|
11679
|
+
statuses,
|
|
11680
|
+
default_status
|
|
11656
11681
|
}
|
|
11657
11682
|
};
|
|
11658
11683
|
}
|
|
@@ -12045,11 +12070,12 @@ var importLeads = {
|
|
|
12045
12070
|
},
|
|
12046
12071
|
statuses: {
|
|
12047
12072
|
type: "object",
|
|
12048
|
-
description:
|
|
12073
|
+
description: `Optional map of raw CSV status-cell text \u2192 lead status (rarely needed). Keys are the verbatim cell strings; values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to {}.`
|
|
12049
12074
|
},
|
|
12050
12075
|
default_status: {
|
|
12051
12076
|
type: ["string", "null"],
|
|
12052
|
-
|
|
12077
|
+
enum: [...LEAD_STATUSES, null],
|
|
12078
|
+
description: `Optional default lead status applied to rows without an explicit status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive). Defaults to null.`
|
|
12053
12079
|
}
|
|
12054
12080
|
},
|
|
12055
12081
|
required: ["fields"]
|
|
@@ -17624,8 +17650,15 @@ var importAndQualify = {
|
|
|
17624
17650
|
type: "object",
|
|
17625
17651
|
description: "Ergonomic shorthand: `{CsvColumn: <number-id>}` or `{CsvColumn: '<field-name>'}` for custom-field mappings. Resolved against /crm/custom_fields catalog."
|
|
17626
17652
|
},
|
|
17627
|
-
statuses: {
|
|
17628
|
-
|
|
17653
|
+
statuses: {
|
|
17654
|
+
type: "object",
|
|
17655
|
+
description: `Optional map of raw CSV status-cell text \u2192 lead status. Values must be one of ${LEAD_STATUSES.join(", ")} (case-insensitive); keys are the verbatim cell strings.`
|
|
17656
|
+
},
|
|
17657
|
+
default_status: {
|
|
17658
|
+
type: ["string", "null"],
|
|
17659
|
+
enum: [...LEAD_STATUSES, null],
|
|
17660
|
+
description: `Optional default lead status. One of ${LEAD_STATUSES.join(", ")} (case-insensitive).`
|
|
17661
|
+
}
|
|
17629
17662
|
},
|
|
17630
17663
|
// mappings has a closed shape (fields/custom_fields/statuses/default_status).
|
|
17631
17664
|
// Inner objects (fields, custom_fields, statuses) keep open shapes
|
|
@@ -22424,6 +22457,25 @@ async function resolveClientFromToken(token, opts = {}) {
|
|
|
22424
22457
|
};
|
|
22425
22458
|
}
|
|
22426
22459
|
}
|
|
22460
|
+
function regionAuthServer(region) {
|
|
22461
|
+
return region === "fr" ? REGIONS.fr : REGIONS.us;
|
|
22462
|
+
}
|
|
22463
|
+
function protectedResourceMetadata(opts) {
|
|
22464
|
+
return {
|
|
22465
|
+
resource: opts.resourceUrl,
|
|
22466
|
+
authorization_servers: [regionAuthServer(opts.region)],
|
|
22467
|
+
bearer_methods_supported: ["header"]
|
|
22468
|
+
};
|
|
22469
|
+
}
|
|
22470
|
+
function buildWwwAuthenticate(opts) {
|
|
22471
|
+
const parts = ['Bearer realm="mcp"'];
|
|
22472
|
+
if (opts.authState === "expired") {
|
|
22473
|
+
parts.push('error="invalid_token"');
|
|
22474
|
+
parts.push('error_description="The access token is invalid or has expired"');
|
|
22475
|
+
}
|
|
22476
|
+
parts.push(`resource_metadata="${opts.resourceMetadataUrl}"`);
|
|
22477
|
+
return parts.join(", ");
|
|
22478
|
+
}
|
|
22427
22479
|
|
|
22428
22480
|
// src/env.ts
|
|
22429
22481
|
function parseWriteEnv(env = process.env) {
|
|
@@ -22439,7 +22491,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
22439
22491
|
}
|
|
22440
22492
|
|
|
22441
22493
|
// src/http-server.ts
|
|
22442
|
-
var VERSION = true ? "0.
|
|
22494
|
+
var VERSION = true ? "0.21.1" : "0.0.0-dev";
|
|
22443
22495
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
22444
22496
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
22445
22497
|
var sseSessions = /* @__PURE__ */ new Map();
|
|
@@ -22461,29 +22513,76 @@ function extractBearer(authHeader) {
|
|
|
22461
22513
|
const m = /^Bearer\s+(.+)$/i.exec(authHeader);
|
|
22462
22514
|
return m ? m[1].trim() : void 0;
|
|
22463
22515
|
}
|
|
22464
|
-
function
|
|
22465
|
-
if (headerValue === "us" || headerValue === "fr") return headerValue;
|
|
22466
|
-
return void 0;
|
|
22467
|
-
}
|
|
22468
|
-
async function buildServerForRequest(token, region) {
|
|
22469
|
-
const resolved = await resolveClientFromToken(token, { region });
|
|
22516
|
+
function buildServerFromClient(client) {
|
|
22470
22517
|
const includeWrite = parseWriteEnv();
|
|
22471
22518
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
22472
|
-
return buildServer(
|
|
22473
|
-
|
|
22474
|
-
|
|
22475
|
-
|
|
22476
|
-
|
|
22519
|
+
return buildServer(client, { version: VERSION, includeWrite, includeAdvanced });
|
|
22520
|
+
}
|
|
22521
|
+
var PRM_PREFIX = "/.well-known/oauth-protected-resource";
|
|
22522
|
+
var RESOURCE_PATHS = ["/mcp", "/fr/mcp", "/sse", "/fr/sse"];
|
|
22523
|
+
function regionForResourcePath(resourcePath) {
|
|
22524
|
+
return /^\/fr(\/|$)/.test(resourcePath) ? "fr" : "us";
|
|
22525
|
+
}
|
|
22526
|
+
function requestOrigin(c) {
|
|
22527
|
+
const url = new URL(c.req.url);
|
|
22528
|
+
const proto = c.req.header("x-forwarded-proto") ?? url.protocol.replace(/:$/, "");
|
|
22529
|
+
const host = c.req.header("host") ?? url.host;
|
|
22530
|
+
return `${proto}://${host}`;
|
|
22531
|
+
}
|
|
22532
|
+
function applyCors(c) {
|
|
22533
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
22534
|
+
c.header("Access-Control-Expose-Headers", "WWW-Authenticate");
|
|
22535
|
+
}
|
|
22536
|
+
function servePrm(c, resourcePath) {
|
|
22537
|
+
applyCors(c);
|
|
22538
|
+
c.header("Cache-Control", "public, max-age=3600");
|
|
22539
|
+
return c.json(
|
|
22540
|
+
protectedResourceMetadata({
|
|
22541
|
+
resourceUrl: `${requestOrigin(c)}${resourcePath}`,
|
|
22542
|
+
region: regionForResourcePath(resourcePath)
|
|
22543
|
+
})
|
|
22544
|
+
);
|
|
22545
|
+
}
|
|
22546
|
+
function sendChallenge(c, resourcePath, authState) {
|
|
22547
|
+
const resourceMetadataUrl = `${requestOrigin(c)}${PRM_PREFIX}${resourcePath}`;
|
|
22548
|
+
applyCors(c);
|
|
22549
|
+
c.header("WWW-Authenticate", buildWwwAuthenticate({ resourceMetadataUrl, authState }));
|
|
22550
|
+
return c.json(
|
|
22551
|
+
{
|
|
22552
|
+
error: authState === "expired" ? "invalid_token" : "unauthorized",
|
|
22553
|
+
error_description: authState === "expired" ? "Access token is invalid or expired. Sign in with Leadbay again." : "Authentication required. Sign in with Leadbay."
|
|
22554
|
+
},
|
|
22555
|
+
401
|
|
22556
|
+
);
|
|
22477
22557
|
}
|
|
22478
22558
|
var app = new Hono();
|
|
22479
22559
|
app.get("/healthz", (c) => c.json({ ok: true, version: VERSION }));
|
|
22560
|
+
app.get(PRM_PREFIX, (c) => servePrm(c, "/mcp"));
|
|
22561
|
+
app.get(`${PRM_PREFIX}/*`, (c) => {
|
|
22562
|
+
const suffix = c.req.path.slice(PRM_PREFIX.length);
|
|
22563
|
+
const resourcePath = RESOURCE_PATHS.includes(suffix) ? suffix : "/mcp";
|
|
22564
|
+
return servePrm(c, resourcePath);
|
|
22565
|
+
});
|
|
22566
|
+
app.options("*", (c) => {
|
|
22567
|
+
applyCors(c);
|
|
22568
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
22569
|
+
c.header(
|
|
22570
|
+
"Access-Control-Allow-Headers",
|
|
22571
|
+
"Authorization, Content-Type, Mcp-Protocol-Version, Mcp-Session-Id"
|
|
22572
|
+
);
|
|
22573
|
+
return c.body(null, 204);
|
|
22574
|
+
});
|
|
22480
22575
|
var MCP_BODY_LIMIT = bodyLimit({ maxSize: 1 * 1024 * 1024 });
|
|
22481
22576
|
app.use("/mcp", MCP_BODY_LIMIT);
|
|
22577
|
+
app.use("/fr/mcp", MCP_BODY_LIMIT);
|
|
22482
22578
|
app.use("/messages", MCP_BODY_LIMIT);
|
|
22483
|
-
|
|
22579
|
+
async function handleStreamable(c, resourcePath) {
|
|
22484
22580
|
const token = extractBearer(c.req.header("authorization"));
|
|
22485
|
-
const
|
|
22486
|
-
|
|
22581
|
+
const resolved = await resolveClientFromToken(token);
|
|
22582
|
+
if (resolved.authState === "missing" || resolved.authState === "expired") {
|
|
22583
|
+
return sendChallenge(c, resourcePath, resolved.authState);
|
|
22584
|
+
}
|
|
22585
|
+
const server = buildServerFromClient(resolved.client);
|
|
22487
22586
|
const transport = new StreamableHTTPServerTransport({
|
|
22488
22587
|
sessionIdGenerator: void 0,
|
|
22489
22588
|
// Return JSON responses instead of SSE so non-SSE clients (e.g. Codex) work.
|
|
@@ -22519,13 +22618,18 @@ app.all("/mcp", async (c) => {
|
|
|
22519
22618
|
server.close().catch(() => {
|
|
22520
22619
|
});
|
|
22521
22620
|
}
|
|
22522
|
-
}
|
|
22523
|
-
app.
|
|
22621
|
+
}
|
|
22622
|
+
app.all("/mcp", (c) => handleStreamable(c, "/mcp"));
|
|
22623
|
+
app.all("/fr/mcp", (c) => handleStreamable(c, "/fr/mcp"));
|
|
22624
|
+
async function handleSse(c, resourcePath) {
|
|
22524
22625
|
const token = extractBearer(c.req.header("authorization"));
|
|
22525
|
-
const
|
|
22626
|
+
const resolved = await resolveClientFromToken(token);
|
|
22627
|
+
if (resolved.authState === "missing" || resolved.authState === "expired") {
|
|
22628
|
+
return sendChallenge(c, resourcePath, resolved.authState);
|
|
22629
|
+
}
|
|
22526
22630
|
const env = c.env;
|
|
22527
22631
|
const transport = new SSEServerTransport("/messages", env.outgoing);
|
|
22528
|
-
const server =
|
|
22632
|
+
const server = buildServerFromClient(resolved.client);
|
|
22529
22633
|
await server.connect(transport);
|
|
22530
22634
|
const sessionId = transport.sessionId;
|
|
22531
22635
|
sseSessions.set(sessionId, { transport, server, createdAt: Date.now() });
|
|
@@ -22535,7 +22639,9 @@ app.get("/sse", async (c) => {
|
|
|
22535
22639
|
});
|
|
22536
22640
|
};
|
|
22537
22641
|
return new Response(null, { headers: { "x-hono-already-sent": "1" } });
|
|
22538
|
-
}
|
|
22642
|
+
}
|
|
22643
|
+
app.get("/sse", (c) => handleSse(c, "/sse"));
|
|
22644
|
+
app.get("/fr/sse", (c) => handleSse(c, "/fr/sse"));
|
|
22539
22645
|
app.post("/messages", async (c) => {
|
|
22540
22646
|
const sessionId = c.req.query("sessionId");
|
|
22541
22647
|
if (!sessionId) {
|
|
@@ -22550,10 +22656,26 @@ app.post("/messages", async (c) => {
|
|
|
22550
22656
|
await session.transport.handlePostMessage(env.incoming, env.outgoing, body);
|
|
22551
22657
|
return new Response(null, { headers: { "x-hono-already-sent": "1" } });
|
|
22552
22658
|
});
|
|
22553
|
-
var
|
|
22554
|
-
|
|
22555
|
-
|
|
22556
|
-
|
|
22659
|
+
var isEntrypoint = (() => {
|
|
22660
|
+
try {
|
|
22661
|
+
const entry = process.argv[1];
|
|
22662
|
+
if (!entry) return false;
|
|
22663
|
+
const entryName = basename(entry).toLowerCase();
|
|
22664
|
+
if (entryName !== "http-server.js" && entryName !== "leadbay-mcp-http") return false;
|
|
22665
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
|
|
22666
|
+
} catch {
|
|
22667
|
+
return false;
|
|
22668
|
+
}
|
|
22669
|
+
})();
|
|
22670
|
+
if (isEntrypoint) {
|
|
22671
|
+
const _boot = randomUUID4();
|
|
22672
|
+
serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
|
|
22673
|
+
process.stderr.write(
|
|
22674
|
+
`leadbay-mcp-http ${VERSION} listening on http://${info.address}:${info.port} (boot=${_boot})
|
|
22557
22675
|
`
|
|
22558
|
-
|
|
22559
|
-
});
|
|
22676
|
+
);
|
|
22677
|
+
});
|
|
22678
|
+
}
|
|
22679
|
+
export {
|
|
22680
|
+
app
|
|
22681
|
+
};
|
|
@@ -1466,7 +1466,7 @@ var init_installer_gui = __esm({
|
|
|
1466
1466
|
init_install_dxt();
|
|
1467
1467
|
init_install_shared();
|
|
1468
1468
|
init_oauth();
|
|
1469
|
-
VERSION = "0.
|
|
1469
|
+
VERSION = "0.21.1";
|
|
1470
1470
|
PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
|
|
1471
1471
|
sessions = /* @__PURE__ */ new Map();
|
|
1472
1472
|
OAUTH_BASE_URLS = {
|
package/dist/installer-gui.js
CHANGED
|
@@ -873,7 +873,7 @@ async function oauthLogin(opts) {
|
|
|
873
873
|
}
|
|
874
874
|
|
|
875
875
|
// installer/installer-gui.ts
|
|
876
|
-
var VERSION = "0.
|
|
876
|
+
var VERSION = "0.21.1";
|
|
877
877
|
var PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
|
|
878
878
|
var sessions = /* @__PURE__ */ new Map();
|
|
879
879
|
var OAUTH_BASE_URLS = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"mcpName": "io.github.leadbay/leadbay-mcp",
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
|
|
6
6
|
"type": "module",
|