@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 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
- ### ChatGPT Desktop / remote-MCP clients
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
- **ChatGPT Desktop**: open Settings → Apps → Add app → paste the URL above.
302
+ - **Claude Desktop**: Settings → Connectors → Add custom connector → paste the URL.
303
+ - **ChatGPT Desktop**: Settings → Apps → Add app → paste the URL.
302
304
 
303
- The server authenticates each request from the `Authorization: Bearer <token>` header your client sends automatically once you sign in via ChatGPT's OAuth prompt. No token to copy-paste, no local Node install needed.
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
- **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.
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
- If your client requires a region header, add `X-Leadbay-Region: us` or `X-Leadbay-Region: fr` to match your Leadbay account's region.
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: mappings.statuses ?? {},
10962
- default_status: mappings.default_status ?? null
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: "Optional status string mapping (rarely needed). Defaults to {}."
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
- description: "Optional default status. Defaults to null."
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: { type: "object", description: "Optional status string mapping." },
17503
- default_status: { type: ["string", "null"], description: "Optional default status." }
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.20.1";
25961
+ var VERSION = "0.21.1";
25932
25962
  var HELP = `
25933
25963
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
25934
25964
 
@@ -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: mappings.statuses ?? {},
11655
- default_status: mappings.default_status ?? null
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: "Optional status string mapping (rarely needed). Defaults to {}."
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
- description: "Optional default status. Defaults to null."
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: { type: "object", description: "Optional status string mapping." },
17628
- default_status: { type: ["string", "null"], description: "Optional default status." }
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.20.1" : "0.0.0-dev";
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 extractRegion(headerValue) {
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(resolved.client, {
22473
- version: VERSION,
22474
- includeWrite,
22475
- includeAdvanced
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
- app.all("/mcp", async (c) => {
22579
+ async function handleStreamable(c, resourcePath) {
22484
22580
  const token = extractBearer(c.req.header("authorization"));
22485
- const region = extractRegion(c.req.header("x-leadbay-region"));
22486
- const server = await buildServerForRequest(token, region);
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.get("/sse", async (c) => {
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 region = extractRegion(c.req.header("x-leadbay-region"));
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 = await buildServerForRequest(token, region);
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 _boot = randomUUID4();
22554
- serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
22555
- process.stderr.write(
22556
- `leadbay-mcp-http ${VERSION} listening on http://${info.address}:${info.port} (boot=${_boot})
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.20.1";
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 = {
@@ -873,7 +873,7 @@ async function oauthLogin(opts) {
873
873
  }
874
874
 
875
875
  // installer/installer-gui.ts
876
- var VERSION = "0.20.1";
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.20.1",
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",