@leadbay/mcp 0.20.1 → 0.21.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.21.0 — 2026-06-16
4
+
5
+ - **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.
6
+ - **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.
7
+
3
8
  ## 0.20.1 — 2026-06-15
4
9
 
5
10
  - **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
@@ -25928,7 +25928,7 @@ var OAUTH_BASE_URLS = {
25928
25928
  fr: "https://staging.api.leadbay.app"
25929
25929
  }
25930
25930
  };
25931
- var VERSION = "0.20.1";
25931
+ var VERSION = "0.21.0";
25932
25932
  var HELP = `
25933
25933
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
25934
25934
 
@@ -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";
@@ -22424,6 +22427,25 @@ async function resolveClientFromToken(token, opts = {}) {
22424
22427
  };
22425
22428
  }
22426
22429
  }
22430
+ function regionAuthServer(region) {
22431
+ return region === "fr" ? REGIONS.fr : REGIONS.us;
22432
+ }
22433
+ function protectedResourceMetadata(opts) {
22434
+ return {
22435
+ resource: opts.resourceUrl,
22436
+ authorization_servers: [regionAuthServer(opts.region)],
22437
+ bearer_methods_supported: ["header"]
22438
+ };
22439
+ }
22440
+ function buildWwwAuthenticate(opts) {
22441
+ const parts = ['Bearer realm="mcp"'];
22442
+ if (opts.authState === "expired") {
22443
+ parts.push('error="invalid_token"');
22444
+ parts.push('error_description="The access token is invalid or has expired"');
22445
+ }
22446
+ parts.push(`resource_metadata="${opts.resourceMetadataUrl}"`);
22447
+ return parts.join(", ");
22448
+ }
22427
22449
 
22428
22450
  // src/env.ts
22429
22451
  function parseWriteEnv(env = process.env) {
@@ -22439,7 +22461,7 @@ function parseWriteEnv(env = process.env) {
22439
22461
  }
22440
22462
 
22441
22463
  // src/http-server.ts
22442
- var VERSION = true ? "0.20.1" : "0.0.0-dev";
22464
+ var VERSION = true ? "0.21.0" : "0.0.0-dev";
22443
22465
  var PORT = Number(process.env.PORT ?? 8080);
22444
22466
  var HOST = process.env.HOST ?? "0.0.0.0";
22445
22467
  var sseSessions = /* @__PURE__ */ new Map();
@@ -22461,29 +22483,76 @@ function extractBearer(authHeader) {
22461
22483
  const m = /^Bearer\s+(.+)$/i.exec(authHeader);
22462
22484
  return m ? m[1].trim() : void 0;
22463
22485
  }
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 });
22486
+ function buildServerFromClient(client) {
22470
22487
  const includeWrite = parseWriteEnv();
22471
22488
  const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
22472
- return buildServer(resolved.client, {
22473
- version: VERSION,
22474
- includeWrite,
22475
- includeAdvanced
22476
- });
22489
+ return buildServer(client, { version: VERSION, includeWrite, includeAdvanced });
22490
+ }
22491
+ var PRM_PREFIX = "/.well-known/oauth-protected-resource";
22492
+ var RESOURCE_PATHS = ["/mcp", "/fr/mcp", "/sse", "/fr/sse"];
22493
+ function regionForResourcePath(resourcePath) {
22494
+ return /^\/fr(\/|$)/.test(resourcePath) ? "fr" : "us";
22495
+ }
22496
+ function requestOrigin(c) {
22497
+ const url = new URL(c.req.url);
22498
+ const proto = c.req.header("x-forwarded-proto") ?? url.protocol.replace(/:$/, "");
22499
+ const host = c.req.header("host") ?? url.host;
22500
+ return `${proto}://${host}`;
22501
+ }
22502
+ function applyCors(c) {
22503
+ c.header("Access-Control-Allow-Origin", "*");
22504
+ c.header("Access-Control-Expose-Headers", "WWW-Authenticate");
22505
+ }
22506
+ function servePrm(c, resourcePath) {
22507
+ applyCors(c);
22508
+ c.header("Cache-Control", "public, max-age=3600");
22509
+ return c.json(
22510
+ protectedResourceMetadata({
22511
+ resourceUrl: `${requestOrigin(c)}${resourcePath}`,
22512
+ region: regionForResourcePath(resourcePath)
22513
+ })
22514
+ );
22515
+ }
22516
+ function sendChallenge(c, resourcePath, authState) {
22517
+ const resourceMetadataUrl = `${requestOrigin(c)}${PRM_PREFIX}${resourcePath}`;
22518
+ applyCors(c);
22519
+ c.header("WWW-Authenticate", buildWwwAuthenticate({ resourceMetadataUrl, authState }));
22520
+ return c.json(
22521
+ {
22522
+ error: authState === "expired" ? "invalid_token" : "unauthorized",
22523
+ error_description: authState === "expired" ? "Access token is invalid or expired. Sign in with Leadbay again." : "Authentication required. Sign in with Leadbay."
22524
+ },
22525
+ 401
22526
+ );
22477
22527
  }
22478
22528
  var app = new Hono();
22479
22529
  app.get("/healthz", (c) => c.json({ ok: true, version: VERSION }));
22530
+ app.get(PRM_PREFIX, (c) => servePrm(c, "/mcp"));
22531
+ app.get(`${PRM_PREFIX}/*`, (c) => {
22532
+ const suffix = c.req.path.slice(PRM_PREFIX.length);
22533
+ const resourcePath = RESOURCE_PATHS.includes(suffix) ? suffix : "/mcp";
22534
+ return servePrm(c, resourcePath);
22535
+ });
22536
+ app.options("*", (c) => {
22537
+ applyCors(c);
22538
+ c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
22539
+ c.header(
22540
+ "Access-Control-Allow-Headers",
22541
+ "Authorization, Content-Type, Mcp-Protocol-Version, Mcp-Session-Id"
22542
+ );
22543
+ return c.body(null, 204);
22544
+ });
22480
22545
  var MCP_BODY_LIMIT = bodyLimit({ maxSize: 1 * 1024 * 1024 });
22481
22546
  app.use("/mcp", MCP_BODY_LIMIT);
22547
+ app.use("/fr/mcp", MCP_BODY_LIMIT);
22482
22548
  app.use("/messages", MCP_BODY_LIMIT);
22483
- app.all("/mcp", async (c) => {
22549
+ async function handleStreamable(c, resourcePath) {
22484
22550
  const token = extractBearer(c.req.header("authorization"));
22485
- const region = extractRegion(c.req.header("x-leadbay-region"));
22486
- const server = await buildServerForRequest(token, region);
22551
+ const resolved = await resolveClientFromToken(token);
22552
+ if (resolved.authState === "missing" || resolved.authState === "expired") {
22553
+ return sendChallenge(c, resourcePath, resolved.authState);
22554
+ }
22555
+ const server = buildServerFromClient(resolved.client);
22487
22556
  const transport = new StreamableHTTPServerTransport({
22488
22557
  sessionIdGenerator: void 0,
22489
22558
  // Return JSON responses instead of SSE so non-SSE clients (e.g. Codex) work.
@@ -22519,13 +22588,18 @@ app.all("/mcp", async (c) => {
22519
22588
  server.close().catch(() => {
22520
22589
  });
22521
22590
  }
22522
- });
22523
- app.get("/sse", async (c) => {
22591
+ }
22592
+ app.all("/mcp", (c) => handleStreamable(c, "/mcp"));
22593
+ app.all("/fr/mcp", (c) => handleStreamable(c, "/fr/mcp"));
22594
+ async function handleSse(c, resourcePath) {
22524
22595
  const token = extractBearer(c.req.header("authorization"));
22525
- const region = extractRegion(c.req.header("x-leadbay-region"));
22596
+ const resolved = await resolveClientFromToken(token);
22597
+ if (resolved.authState === "missing" || resolved.authState === "expired") {
22598
+ return sendChallenge(c, resourcePath, resolved.authState);
22599
+ }
22526
22600
  const env = c.env;
22527
22601
  const transport = new SSEServerTransport("/messages", env.outgoing);
22528
- const server = await buildServerForRequest(token, region);
22602
+ const server = buildServerFromClient(resolved.client);
22529
22603
  await server.connect(transport);
22530
22604
  const sessionId = transport.sessionId;
22531
22605
  sseSessions.set(sessionId, { transport, server, createdAt: Date.now() });
@@ -22535,7 +22609,9 @@ app.get("/sse", async (c) => {
22535
22609
  });
22536
22610
  };
22537
22611
  return new Response(null, { headers: { "x-hono-already-sent": "1" } });
22538
- });
22612
+ }
22613
+ app.get("/sse", (c) => handleSse(c, "/sse"));
22614
+ app.get("/fr/sse", (c) => handleSse(c, "/fr/sse"));
22539
22615
  app.post("/messages", async (c) => {
22540
22616
  const sessionId = c.req.query("sessionId");
22541
22617
  if (!sessionId) {
@@ -22550,10 +22626,26 @@ app.post("/messages", async (c) => {
22550
22626
  await session.transport.handlePostMessage(env.incoming, env.outgoing, body);
22551
22627
  return new Response(null, { headers: { "x-hono-already-sent": "1" } });
22552
22628
  });
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})
22629
+ var isEntrypoint = (() => {
22630
+ try {
22631
+ const entry = process.argv[1];
22632
+ if (!entry) return false;
22633
+ const entryName = basename(entry).toLowerCase();
22634
+ if (entryName !== "http-server.js" && entryName !== "leadbay-mcp-http") return false;
22635
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
22636
+ } catch {
22637
+ return false;
22638
+ }
22639
+ })();
22640
+ if (isEntrypoint) {
22641
+ const _boot = randomUUID4();
22642
+ serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
22643
+ process.stderr.write(
22644
+ `leadbay-mcp-http ${VERSION} listening on http://${info.address}:${info.port} (boot=${_boot})
22557
22645
  `
22558
- );
22559
- });
22646
+ );
22647
+ });
22648
+ }
22649
+ export {
22650
+ app
22651
+ };
@@ -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.0";
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.0";
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.0",
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",