@cavuno/board 4.64.0 → 4.65.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/{_spec-D1Qg_Twd.d.mts → _spec-CIufstif.d.mts} +57 -1
  3. package/dist/{_spec-D1Qg_Twd.d.ts → _spec-CIufstif.d.ts} +57 -1
  4. package/dist/{blog-Dp2jAVVM.d.ts → blog-DuNJLfIh.d.ts} +1 -1
  5. package/dist/{blog-e0YbZBjg.d.mts → blog-teDJ9KaO.d.mts} +1 -1
  6. package/dist/{board-Cc3wbTKI.d.mts → board-DZNCHGgl.d.mts} +2 -2
  7. package/dist/{board-BeNZLOYC.d.ts → board-iCHYuNhr.d.ts} +2 -2
  8. package/dist/{breadcrumbs-C5WjB8jm.d.mts → breadcrumbs-Bt3oNIAx.d.mts} +2 -2
  9. package/dist/{breadcrumbs-BOFeIYEX.d.ts → breadcrumbs-DYGo0yB9.d.ts} +2 -2
  10. package/dist/browser/cavuno-board.global.min.js +1 -1
  11. package/dist/chunk-RDHHRDKN.mjs +6 -0
  12. package/dist/doctor.js +1 -1
  13. package/dist/filters.d.mts +2 -2
  14. package/dist/filters.d.ts +2 -2
  15. package/dist/format.d.mts +4 -4
  16. package/dist/format.d.ts +4 -4
  17. package/dist/index.d.mts +15 -14
  18. package/dist/index.d.ts +15 -14
  19. package/dist/index.js +1 -1
  20. package/dist/index.mjs +4 -4
  21. package/dist/{jobs-CClqlUld.d.mts → jobs-CbXbDBij.d.mts} +1 -1
  22. package/dist/{jobs-D04ngjCP.d.ts → jobs-DM_QMccX.d.ts} +1 -1
  23. package/dist/mcp.d.mts +52 -0
  24. package/dist/mcp.d.ts +52 -0
  25. package/dist/mcp.js +150 -0
  26. package/dist/mcp.mjs +128 -0
  27. package/dist/{search-DZeNdYN6.d.ts → search-B7x4qt9A.d.ts} +1 -1
  28. package/dist/{search-BtrwcLkc.d.mts → search-PrqcJMft.d.mts} +1 -1
  29. package/dist/{seo-DLoyZRTo.d.ts → seo-C1Br7ygq.d.ts} +1 -1
  30. package/dist/seo-files.d.mts +2 -2
  31. package/dist/seo-files.d.ts +2 -2
  32. package/dist/{seo-LfpgbdnO.d.mts → seo-ohehUCzN.d.mts} +1 -1
  33. package/dist/seo.d.mts +22 -7
  34. package/dist/seo.d.ts +22 -7
  35. package/dist/seo.js +12 -3
  36. package/dist/seo.mjs +12 -3
  37. package/dist/server.d.mts +6 -6
  38. package/dist/server.d.ts +6 -6
  39. package/dist/sitemap.d.mts +6 -6
  40. package/dist/sitemap.d.ts +6 -6
  41. package/dist/skills.js +1 -1
  42. package/dist/suggest.d.mts +2 -2
  43. package/dist/suggest.d.ts +2 -2
  44. package/package.json +13 -2
  45. package/skills/cavuno-board-mcp/SKILL.md +80 -0
  46. package/skills/cavuno-board-seo/BLOG.md +13 -8
  47. package/skills/manifest.json +8 -1
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { S as Schemas, c as components } from './_spec-D1Qg_Twd.js';
2
- import { F as FetchOptions, S as StorageMode, B as BoardRequest, A as Awaitable, L as Logger, a as BoardClient, b as SearchSuggestQuery } from './search-DZeNdYN6.js';
3
- export { c as ACCESS_TOKEN_KEY, d as BOARD_ACCESS_GRANT_KEY, C as CompanySuggestion, e as CustomStorage, M as MarketSuggestion, R as REFRESH_TOKEN_KEY, f as SearchSuggestType, g as SuggestResult, h as SuggestionItem, T as TermSuggestion } from './search-DZeNdYN6.js';
4
- import { L as ListEnvelope, R as RemoteOption, E as EmploymentType, S as Seniority, a as JobCapability, b as CustomFieldFilter, O as ObjectReferenceFilter, c as JobCollectionChoiceQuery, d as JobsListQuery, e as JobCardListEnvelope, f as JobRetrieveQuery, g as JobsSearchBody, h as JobCardSearchEnvelope, i as JobsSimilarQuery, j as SearchEnvelope } from './jobs-D04ngjCP.js';
5
- export { k as CustomFieldValue, C as CustomFieldValues, l as CustomFilterValue, m as EducationRequirement, n as JobCatalogPagination, o as JobCollectionChoiceList, p as JobCollectionFieldDefinition, q as JobCompany, r as JobCustomEmploymentType, J as JobSort, s as OfficeLocation, t as OffsetPagination, P as PublicJob, u as PublicJobCard, v as RelatedSearch, w as RemotePermit, x as RemoteTimezone, y as ResolvedJobCollectionField, z as StorefrontPagination } from './jobs-D04ngjCP.js';
6
- import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, b as CompanySalary } from './board-BeNZLOYC.js';
7
- export { B as BoardCustomEmploymentType, j as BoardFormBuiltinField, k as BoardFormField, l as BoardFormLayout, m as BoardFormLockReason, n as BoardJobFormField, o as BoardProfileFormField, p as BoardRoleSignIn, q as BoardSignInSsoConnection, a as CompanyCategorySalary, r as CompanyMarket, s as CompanyMarketRef, t as CompanySalarySummary, C as CustomFieldDefinition, u as CustomFieldOption, v as CustomFieldType, L as LocationSalaryDetail, w as LocationSkillsIndex, x as LocationTitlesIndex, P as PublicBoard, y as PublicBoardAds, z as PublicBoardAnalytics, A as PublicBoardFeatures, D as PublicBoardJobForm, E as PublicBoardSignIn, F as PublicCompany, G as PublicCompanyDetail, H as PublicCompanyMembership, I as SalaryCompany, J as SalaryLocation, K as SalarySkill, M as SalaryTitle, N as SkillLocationSalary, O as SkillLocationsIndex, S as SkillSalaryDetail, Q as TitleLocationSalary, R as TitleLocationsIndex, T as TitleSalaryDetail } from './board-BeNZLOYC.js';
8
- export { B as BoardSeo } from './seo-DLoyZRTo.js';
9
- import { a as BlogPostsListQuery, b as BlogSimilarQuery, c as BlogSearchBody } from './blog-Dp2jAVVM.js';
10
- export { B as BlogAuthorEmbed, d as BlogTagEmbed, e as PublicBlogAdjacentPosts, f as PublicBlogAuthor, g as PublicBlogPost, P as PublicBlogPostSummary, h as PublicBlogTag } from './blog-Dp2jAVVM.js';
1
+ import { S as Schemas, c as components } from './_spec-CIufstif.js';
2
+ import { F as FetchOptions, S as StorageMode, B as BoardRequest, A as Awaitable, L as Logger, a as BoardClient, b as SearchSuggestQuery } from './search-B7x4qt9A.js';
3
+ export { c as ACCESS_TOKEN_KEY, d as BOARD_ACCESS_GRANT_KEY, C as CompanySuggestion, e as CustomStorage, M as MarketSuggestion, R as REFRESH_TOKEN_KEY, f as SearchSuggestType, g as SuggestResult, h as SuggestionItem, T as TermSuggestion } from './search-B7x4qt9A.js';
4
+ import { L as ListEnvelope, R as RemoteOption, E as EmploymentType, S as Seniority, a as JobCapability, b as CustomFieldFilter, O as ObjectReferenceFilter, c as JobCollectionChoiceQuery, d as JobsListQuery, e as JobCardListEnvelope, f as JobRetrieveQuery, g as JobsSearchBody, h as JobCardSearchEnvelope, i as JobsSimilarQuery, j as SearchEnvelope } from './jobs-DM_QMccX.js';
5
+ export { k as CustomFieldValue, C as CustomFieldValues, l as CustomFilterValue, m as EducationRequirement, n as JobCatalogPagination, o as JobCollectionChoiceList, p as JobCollectionFieldDefinition, q as JobCompany, r as JobCustomEmploymentType, J as JobSort, s as OfficeLocation, t as OffsetPagination, P as PublicJob, u as PublicJobCard, v as RelatedSearch, w as RemotePermit, x as RemoteTimezone, y as ResolvedJobCollectionField, z as StorefrontPagination } from './jobs-DM_QMccX.js';
6
+ import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, b as CompanySalary } from './board-iCHYuNhr.js';
7
+ export { B as BoardCustomEmploymentType, j as BoardFormBuiltinField, k as BoardFormField, l as BoardFormLayout, m as BoardFormLockReason, n as BoardJobFormField, o as BoardProfileFormField, p as BoardRoleSignIn, q as BoardSignInSsoConnection, a as CompanyCategorySalary, r as CompanyMarket, s as CompanyMarketRef, t as CompanySalarySummary, C as CustomFieldDefinition, u as CustomFieldOption, v as CustomFieldType, L as LocationSalaryDetail, w as LocationSkillsIndex, x as LocationTitlesIndex, P as PublicBoard, y as PublicBoardAds, z as PublicBoardAnalytics, A as PublicBoardFeatures, D as PublicBoardJobForm, E as PublicBoardSignIn, F as PublicCompany, G as PublicCompanyDetail, H as PublicCompanyMembership, I as SalaryCompany, J as SalaryLocation, K as SalarySkill, M as SalaryTitle, N as SkillLocationSalary, O as SkillLocationsIndex, S as SkillSalaryDetail, Q as TitleLocationSalary, R as TitleLocationsIndex, T as TitleSalaryDetail } from './board-iCHYuNhr.js';
8
+ export { B as BoardSeo } from './seo-C1Br7ygq.js';
9
+ import { a as BlogPostsListQuery, b as BlogSimilarQuery, c as BlogSearchBody } from './blog-DuNJLfIh.js';
10
+ export { B as BlogAuthorEmbed, d as BlogTagEmbed, e as PublicBlogAdjacentPosts, f as PublicBlogAuthor, g as PublicBlogPost, P as PublicBlogPostSummary, h as PublicBlogTag } from './blog-DuNJLfIh.js';
11
11
 
12
12
  /**
13
13
  * Well-known `<domain>_<snake_reason>` codes the Board API sends, grouped by
@@ -202,7 +202,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
202
202
  * constant because the package is platform-neutral and cannot read
203
203
  * package.json at runtime.
204
204
  */
205
- declare const SDK_VERSION = "4.64.0";
205
+ declare const SDK_VERSION = "4.65.0";
206
206
 
207
207
  type SavedJob = Schemas['SavedJob'];
208
208
  type SavedJobsListQuery = {
@@ -1613,6 +1613,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
1613
1613
  messaging: boolean;
1614
1614
  contactPage: boolean;
1615
1615
  placeSuggestions: boolean;
1616
+ mcp: boolean;
1616
1617
  };
1617
1618
  talentAccessModel: "paid_messaging" | "paid_unlocks_and_messaging" | null;
1618
1619
  posting: {
@@ -3795,7 +3796,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
3795
3796
  object: "marketing_consent";
3796
3797
  status: "granted" | "withdrawn";
3797
3798
  source: string;
3798
- reason: "person_request" | "operator_request" | "account_deleted" | null;
3799
+ reason: "person_request" | "operator_request" | "account_deleted" | "spam_report" | null;
3799
3800
  grantedAt: string | null;
3800
3801
  withdrawnAt: string | null;
3801
3802
  revision: number;
@@ -3805,7 +3806,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
3805
3806
  object: "marketing_consent";
3806
3807
  status: "granted" | "withdrawn";
3807
3808
  source: string;
3808
- reason: "person_request" | "operator_request" | "account_deleted" | null;
3809
+ reason: "person_request" | "operator_request" | "account_deleted" | "spam_report" | null;
3809
3810
  grantedAt: string | null;
3810
3811
  withdrawnAt: string | null;
3811
3812
  revision: number;
@@ -3815,7 +3816,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
3815
3816
  object: "marketing_consent";
3816
3817
  status: "granted" | "withdrawn";
3817
3818
  source: string;
3818
- reason: "person_request" | "operator_request" | "account_deleted" | null;
3819
+ reason: "person_request" | "operator_request" | "account_deleted" | "spam_report" | null;
3819
3820
  grantedAt: string | null;
3820
3821
  withdrawnAt: string | null;
3821
3822
  revision: number;
package/dist/index.js CHANGED
@@ -441,7 +441,7 @@ async function clearSession(storage) {
441
441
  }
442
442
 
443
443
  // src/version.ts
444
- var SDK_VERSION = "4.64.0";
444
+ var SDK_VERSION = "4.65.0";
445
445
 
446
446
  // src/client.ts
447
447
  function isRawBody(body) {
package/dist/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ DEFAULT_CAVUNO_API_URL
3
+ } from "./chunk-RDHHRDKN.mjs";
1
4
  import {
2
5
  ACCESS_TOKEN_KEY,
3
6
  BOARD_ACCESS_GRANT_KEY,
@@ -46,7 +49,7 @@ function toSearchParams(query) {
46
49
  }
47
50
 
48
51
  // src/version.ts
49
- var SDK_VERSION = "4.64.0";
52
+ var SDK_VERSION = "4.65.0";
50
53
 
51
54
  // src/client.ts
52
55
  function isRawBody(body) {
@@ -150,9 +153,6 @@ var BoardClient = class {
150
153
  }
151
154
  };
152
155
 
153
- // src/constants.ts
154
- var DEFAULT_CAVUNO_API_URL = "https://api.cavuno.com";
155
-
156
156
  // src/sso.ts
157
157
  var SIGN_IN_REDIRECT_ERROR_CODES = [
158
158
  "oauth_cancelled",
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.mjs';
1
+ import { S as Schemas } from './_spec-CIufstif.mjs';
2
2
 
3
3
  /**
4
4
  * Optional pagination metadata returned by Board API list operations.
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.js';
1
+ import { S as Schemas } from './_spec-CIufstif.js';
2
2
 
3
3
  /**
4
4
  * Optional pagination metadata returned by Board API list operations.
package/dist/mcp.d.mts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The board's public MCP server on a self-hosted board's own host.
3
+ *
4
+ * Mount the handler at `/mcp`, `/mcp/*` and `/.well-known/ai-catalog.json`.
5
+ * It forwards to the Board API's `POST /v1/boards/{board}/mcp` and its
6
+ * `GET /v1/boards/{board}/mcp/docs`, `/mcp/server-card` and `/mcp/ai-catalog`,
7
+ * the same endpoints Cavuno-hosted boards answer from, so the tools, limits,
8
+ * setup page and discovery documents stay identical everywhere:
9
+ *
10
+ * - `POST /mcp` is the stateless Streamable HTTP transport.
11
+ * - A browser opening `/mcp` is sent to `/mcp/docs`; any other method is 405.
12
+ * - `GET|HEAD /mcp/docs` is the HTML setup page listed in AI directories.
13
+ * - `GET|HEAD /mcp/server-card` is the MCP Server Card: the server's name,
14
+ * version and URL, for an assistant to read before it connects.
15
+ * - `GET|HEAD /.well-known/ai-catalog.json` is the AI Catalog listing that
16
+ * card, so an assistant can find the server from the board's domain alone.
17
+ *
18
+ * Only the MCP transport headers are forwarded: no cookies and no
19
+ * authorization, because every tool is an anonymous read. The Board API's
20
+ * per-visitor limit sees this server's address, so it applies to all of the
21
+ * board's MCP traffic together.
22
+ *
23
+ * Framework-portable: standard Request in, standard Response out.
24
+ */
25
+ interface CreateMcpHandlerConfig {
26
+ /**
27
+ * The board's publishable key (`pk_…`). Job and company links in tool
28
+ * results then point at the key's own origin, this board.
29
+ */
30
+ board: string;
31
+ /** Board API origin. Defaults to `https://api.cavuno.com`. */
32
+ baseUrl?: string;
33
+ }
34
+ /**
35
+ * Create the `(request) => Response` handler for `/mcp`, `/mcp/docs`,
36
+ * `/mcp/server-card` and `/.well-known/ai-catalog.json`.
37
+ *
38
+ * @example
39
+ * // Next.js App Router: app/mcp/[[...path]]/route.ts and
40
+ * // app/.well-known/ai-catalog.json/route.ts, both exporting:
41
+ * import { createMcpHandler } from '@cavuno/board/mcp';
42
+ *
43
+ * const handler = createMcpHandler({
44
+ * board: process.env.NEXT_PUBLIC_CAVUNO_BOARD_KEY!,
45
+ * });
46
+ * export const GET = handler;
47
+ * export const HEAD = handler;
48
+ * export const POST = handler;
49
+ */
50
+ declare function createMcpHandler(config: CreateMcpHandlerConfig): (request: Request) => Promise<Response>;
51
+
52
+ export { type CreateMcpHandlerConfig, createMcpHandler };
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The board's public MCP server on a self-hosted board's own host.
3
+ *
4
+ * Mount the handler at `/mcp`, `/mcp/*` and `/.well-known/ai-catalog.json`.
5
+ * It forwards to the Board API's `POST /v1/boards/{board}/mcp` and its
6
+ * `GET /v1/boards/{board}/mcp/docs`, `/mcp/server-card` and `/mcp/ai-catalog`,
7
+ * the same endpoints Cavuno-hosted boards answer from, so the tools, limits,
8
+ * setup page and discovery documents stay identical everywhere:
9
+ *
10
+ * - `POST /mcp` is the stateless Streamable HTTP transport.
11
+ * - A browser opening `/mcp` is sent to `/mcp/docs`; any other method is 405.
12
+ * - `GET|HEAD /mcp/docs` is the HTML setup page listed in AI directories.
13
+ * - `GET|HEAD /mcp/server-card` is the MCP Server Card: the server's name,
14
+ * version and URL, for an assistant to read before it connects.
15
+ * - `GET|HEAD /.well-known/ai-catalog.json` is the AI Catalog listing that
16
+ * card, so an assistant can find the server from the board's domain alone.
17
+ *
18
+ * Only the MCP transport headers are forwarded: no cookies and no
19
+ * authorization, because every tool is an anonymous read. The Board API's
20
+ * per-visitor limit sees this server's address, so it applies to all of the
21
+ * board's MCP traffic together.
22
+ *
23
+ * Framework-portable: standard Request in, standard Response out.
24
+ */
25
+ interface CreateMcpHandlerConfig {
26
+ /**
27
+ * The board's publishable key (`pk_…`). Job and company links in tool
28
+ * results then point at the key's own origin, this board.
29
+ */
30
+ board: string;
31
+ /** Board API origin. Defaults to `https://api.cavuno.com`. */
32
+ baseUrl?: string;
33
+ }
34
+ /**
35
+ * Create the `(request) => Response` handler for `/mcp`, `/mcp/docs`,
36
+ * `/mcp/server-card` and `/.well-known/ai-catalog.json`.
37
+ *
38
+ * @example
39
+ * // Next.js App Router: app/mcp/[[...path]]/route.ts and
40
+ * // app/.well-known/ai-catalog.json/route.ts, both exporting:
41
+ * import { createMcpHandler } from '@cavuno/board/mcp';
42
+ *
43
+ * const handler = createMcpHandler({
44
+ * board: process.env.NEXT_PUBLIC_CAVUNO_BOARD_KEY!,
45
+ * });
46
+ * export const GET = handler;
47
+ * export const HEAD = handler;
48
+ * export const POST = handler;
49
+ */
50
+ declare function createMcpHandler(config: CreateMcpHandlerConfig): (request: Request) => Promise<Response>;
51
+
52
+ export { type CreateMcpHandlerConfig, createMcpHandler };
package/dist/mcp.js ADDED
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/mcp/index.ts
21
+ var mcp_exports = {};
22
+ __export(mcp_exports, {
23
+ createMcpHandler: () => createMcpHandler
24
+ });
25
+ module.exports = __toCommonJS(mcp_exports);
26
+
27
+ // src/constants.ts
28
+ var DEFAULT_CAVUNO_API_URL = "https://api.cavuno.com";
29
+
30
+ // src/mcp/create-mcp-handler.ts
31
+ var DOCUMENT_UPSTREAMS = [
32
+ ["/mcp/docs", "mcp/docs"],
33
+ ["/mcp/server-card", "mcp/server-card"],
34
+ ["/.well-known/ai-catalog.json", "mcp/ai-catalog"]
35
+ ];
36
+ var MAX_BODY_BYTES = 64 * 1024;
37
+ var UPSTREAM_TIMEOUT_MS = 3e4;
38
+ var FORWARDED_HEADERS = [
39
+ "content-type",
40
+ "accept",
41
+ "mcp-protocol-version",
42
+ "user-agent"
43
+ ];
44
+ function jsonRpcError(status, message, headers = {}) {
45
+ return new Response(
46
+ JSON.stringify({
47
+ jsonrpc: "2.0",
48
+ id: null,
49
+ error: { code: -32e3, message }
50
+ }),
51
+ {
52
+ status,
53
+ headers: { "content-type": "application/json", ...headers }
54
+ }
55
+ );
56
+ }
57
+ async function readCappedBody(request) {
58
+ if (Number(request.headers.get("content-length")) > MAX_BODY_BYTES) {
59
+ return null;
60
+ }
61
+ if (!request.body) return new Uint8Array();
62
+ const reader = request.body.getReader();
63
+ const chunks = [];
64
+ let size = 0;
65
+ for (; ; ) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+ size += value.byteLength;
69
+ if (size > MAX_BODY_BYTES) {
70
+ await reader.cancel();
71
+ return null;
72
+ }
73
+ chunks.push(value);
74
+ }
75
+ const body = new Uint8Array(size);
76
+ let offset = 0;
77
+ for (const chunk of chunks) {
78
+ body.set(chunk, offset);
79
+ offset += chunk.byteLength;
80
+ }
81
+ return body;
82
+ }
83
+ function createMcpHandler(config) {
84
+ const base = `${(config.baseUrl ?? DEFAULT_CAVUNO_API_URL).replace(/\/+$/, "")}/v1/boards/${encodeURIComponent(config.board)}`;
85
+ return async function mcpHandler(request) {
86
+ const { pathname } = new URL(request.url);
87
+ const path = pathname.replace(/\/+$/, "");
88
+ const method = request.method;
89
+ let upstream;
90
+ const document = DOCUMENT_UPSTREAMS.find(
91
+ ([suffix]) => path.endsWith(suffix)
92
+ );
93
+ if (document) {
94
+ if (method !== "GET" && method !== "HEAD") {
95
+ return new Response(null, {
96
+ status: 405,
97
+ headers: { allow: "GET, HEAD" }
98
+ });
99
+ }
100
+ upstream = `${base}/${document[1]}`;
101
+ } else if (!path.endsWith("/mcp")) {
102
+ return new Response(null, { status: 404 });
103
+ } else {
104
+ const accept = request.headers.get("accept") ?? "";
105
+ if (method === "GET" && accept.includes("text/html") && !accept.includes("text/event-stream")) {
106
+ return new Response(null, {
107
+ status: 302,
108
+ headers: { location: `${path}/docs`, "cache-control": "no-store" }
109
+ });
110
+ }
111
+ if (method !== "POST") {
112
+ return jsonRpcError(405, "Method not allowed.", { allow: "POST" });
113
+ }
114
+ upstream = `${base}/mcp`;
115
+ }
116
+ let body;
117
+ if (method === "POST") {
118
+ const read = await readCappedBody(request);
119
+ if (!read) return jsonRpcError(413, "Request body too large.");
120
+ body = read;
121
+ }
122
+ const headers = new Headers();
123
+ for (const name of FORWARDED_HEADERS) {
124
+ const value = request.headers.get(name);
125
+ if (value) headers.set(name, value);
126
+ }
127
+ let response;
128
+ try {
129
+ response = await fetch(upstream, {
130
+ method,
131
+ headers,
132
+ body,
133
+ redirect: "manual",
134
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
135
+ });
136
+ } catch {
137
+ return jsonRpcError(503, "The board MCP server is unavailable.", {
138
+ "retry-after": "30"
139
+ });
140
+ }
141
+ const outHeaders = new Headers(response.headers);
142
+ outHeaders.delete("set-cookie");
143
+ outHeaders.delete("content-encoding");
144
+ outHeaders.delete("content-length");
145
+ return new Response(response.body, {
146
+ status: response.status,
147
+ headers: outHeaders
148
+ });
149
+ };
150
+ }
package/dist/mcp.mjs ADDED
@@ -0,0 +1,128 @@
1
+ import {
2
+ DEFAULT_CAVUNO_API_URL
3
+ } from "./chunk-RDHHRDKN.mjs";
4
+
5
+ // src/mcp/create-mcp-handler.ts
6
+ var DOCUMENT_UPSTREAMS = [
7
+ ["/mcp/docs", "mcp/docs"],
8
+ ["/mcp/server-card", "mcp/server-card"],
9
+ ["/.well-known/ai-catalog.json", "mcp/ai-catalog"]
10
+ ];
11
+ var MAX_BODY_BYTES = 64 * 1024;
12
+ var UPSTREAM_TIMEOUT_MS = 3e4;
13
+ var FORWARDED_HEADERS = [
14
+ "content-type",
15
+ "accept",
16
+ "mcp-protocol-version",
17
+ "user-agent"
18
+ ];
19
+ function jsonRpcError(status, message, headers = {}) {
20
+ return new Response(
21
+ JSON.stringify({
22
+ jsonrpc: "2.0",
23
+ id: null,
24
+ error: { code: -32e3, message }
25
+ }),
26
+ {
27
+ status,
28
+ headers: { "content-type": "application/json", ...headers }
29
+ }
30
+ );
31
+ }
32
+ async function readCappedBody(request) {
33
+ if (Number(request.headers.get("content-length")) > MAX_BODY_BYTES) {
34
+ return null;
35
+ }
36
+ if (!request.body) return new Uint8Array();
37
+ const reader = request.body.getReader();
38
+ const chunks = [];
39
+ let size = 0;
40
+ for (; ; ) {
41
+ const { done, value } = await reader.read();
42
+ if (done) break;
43
+ size += value.byteLength;
44
+ if (size > MAX_BODY_BYTES) {
45
+ await reader.cancel();
46
+ return null;
47
+ }
48
+ chunks.push(value);
49
+ }
50
+ const body = new Uint8Array(size);
51
+ let offset = 0;
52
+ for (const chunk of chunks) {
53
+ body.set(chunk, offset);
54
+ offset += chunk.byteLength;
55
+ }
56
+ return body;
57
+ }
58
+ function createMcpHandler(config) {
59
+ const base = `${(config.baseUrl ?? DEFAULT_CAVUNO_API_URL).replace(/\/+$/, "")}/v1/boards/${encodeURIComponent(config.board)}`;
60
+ return async function mcpHandler(request) {
61
+ const { pathname } = new URL(request.url);
62
+ const path = pathname.replace(/\/+$/, "");
63
+ const method = request.method;
64
+ let upstream;
65
+ const document = DOCUMENT_UPSTREAMS.find(
66
+ ([suffix]) => path.endsWith(suffix)
67
+ );
68
+ if (document) {
69
+ if (method !== "GET" && method !== "HEAD") {
70
+ return new Response(null, {
71
+ status: 405,
72
+ headers: { allow: "GET, HEAD" }
73
+ });
74
+ }
75
+ upstream = `${base}/${document[1]}`;
76
+ } else if (!path.endsWith("/mcp")) {
77
+ return new Response(null, { status: 404 });
78
+ } else {
79
+ const accept = request.headers.get("accept") ?? "";
80
+ if (method === "GET" && accept.includes("text/html") && !accept.includes("text/event-stream")) {
81
+ return new Response(null, {
82
+ status: 302,
83
+ headers: { location: `${path}/docs`, "cache-control": "no-store" }
84
+ });
85
+ }
86
+ if (method !== "POST") {
87
+ return jsonRpcError(405, "Method not allowed.", { allow: "POST" });
88
+ }
89
+ upstream = `${base}/mcp`;
90
+ }
91
+ let body;
92
+ if (method === "POST") {
93
+ const read = await readCappedBody(request);
94
+ if (!read) return jsonRpcError(413, "Request body too large.");
95
+ body = read;
96
+ }
97
+ const headers = new Headers();
98
+ for (const name of FORWARDED_HEADERS) {
99
+ const value = request.headers.get(name);
100
+ if (value) headers.set(name, value);
101
+ }
102
+ let response;
103
+ try {
104
+ response = await fetch(upstream, {
105
+ method,
106
+ headers,
107
+ body,
108
+ redirect: "manual",
109
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
110
+ });
111
+ } catch {
112
+ return jsonRpcError(503, "The board MCP server is unavailable.", {
113
+ "retry-after": "30"
114
+ });
115
+ }
116
+ const outHeaders = new Headers(response.headers);
117
+ outHeaders.delete("set-cookie");
118
+ outHeaders.delete("content-encoding");
119
+ outHeaders.delete("content-length");
120
+ return new Response(response.body, {
121
+ status: response.status,
122
+ headers: outHeaders
123
+ });
124
+ };
125
+ }
126
+ export {
127
+ createMcpHandler
128
+ };
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.js';
1
+ import { S as Schemas } from './_spec-CIufstif.js';
2
2
 
3
3
  type Awaitable<T> = T | Promise<T>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.mjs';
1
+ import { S as Schemas } from './_spec-CIufstif.mjs';
2
2
 
3
3
  type Awaitable<T> = T | Promise<T>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.js';
1
+ import { S as Schemas } from './_spec-CIufstif.js';
2
2
 
3
3
  /**
4
4
  * The public SEO-infra payload (`board.seo()`) — platform infrastructure a
@@ -1,5 +1,5 @@
1
- import { B as BoardSeo } from './seo-LfpgbdnO.mjs';
2
- import './_spec-D1Qg_Twd.mjs';
1
+ import { B as BoardSeo } from './seo-ohehUCzN.mjs';
2
+ import './_spec-CIufstif.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/seo-files` — platform-owned crawler/identity file bodies
@@ -1,5 +1,5 @@
1
- import { B as BoardSeo } from './seo-DLoyZRTo.js';
2
- import './_spec-D1Qg_Twd.js';
1
+ import { B as BoardSeo } from './seo-C1Br7ygq.js';
2
+ import './_spec-CIufstif.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/seo-files` — platform-owned crawler/identity file bodies
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.mjs';
1
+ import { S as Schemas } from './_spec-CIufstif.mjs';
2
2
 
3
3
  /**
4
4
  * The public SEO-infra payload (`board.seo()`) — platform infrastructure a
package/dist/seo.d.mts CHANGED
@@ -1,9 +1,9 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-e0YbZBjg.mjs';
2
- import { e as JsonLdObject } from './breadcrumbs-C5WjB8jm.mjs';
3
- export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-C5WjB8jm.mjs';
4
- import './_spec-D1Qg_Twd.mjs';
5
- import './board-Cc3wbTKI.mjs';
6
- import './jobs-CClqlUld.mjs';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-teDJ9KaO.mjs';
2
+ import { e as JsonLdObject } from './breadcrumbs-Bt3oNIAx.mjs';
3
+ export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-Bt3oNIAx.mjs';
4
+ import './_spec-CIufstif.mjs';
5
+ import './board-DZNCHGgl.mjs';
6
+ import './jobs-CbXbDBij.mjs';
7
7
 
8
8
  /**
9
9
  * Blog structured data on the `@cavuno/board` wire types, transcribed from
@@ -28,13 +28,24 @@ type ArticleJsonLdPost = Pick<PublicBlogPostSummary, 'title' | 'customExcerpt' |
28
28
  * first, then `ogImageUrl` — pass your page's OG image URL (hosted passes
29
29
  * its generated `/blog/:slug/og` card; OG generation itself is app-owned
30
30
  * per ).
31
+ *
32
+ * Pass `authorProfileUrl` (the first author's profile page URL) so the
33
+ * `author` is the same `Person` (`@id` + `url`) that
34
+ * `createAuthorProfileJsonLd` declares on that page. Without it the author
35
+ * carries only a name and their website, and nothing links the post to the
36
+ * profile.
31
37
  */
32
- declare function createBlogArticleJsonLd({ post, boardName, permalink, ogImageUrl, }: {
38
+ declare function createBlogArticleJsonLd({ post, boardName, permalink, ogImageUrl, authorProfileUrl, }: {
33
39
  post: ArticleJsonLdPost;
34
40
  boardName: string;
35
41
  permalink: string;
36
42
  /** Fallback image when the post has no cover, e.g. your generated OG card URL. */
37
43
  ogImageUrl?: string | null;
44
+ /**
45
+ * Absolute canonical URL of the first author's profile page: the
46
+ * `canonical` you pass to `createAuthorProfileJsonLd` on that page.
47
+ */
48
+ authorProfileUrl?: string | null;
38
49
  }): JsonLdObject;
39
50
  /**
40
51
  * `ProfilePage` for a blog author page — the hosted shape: a `Person` main
@@ -55,6 +66,10 @@ declare function createAuthorProfileJsonLd({ author, canonical, description, ori
55
66
  origin: string | null;
56
67
  /** The author's recent posts (newest first) — the builder keeps the first 5. */
57
68
  posts: Pick<PublicBlogPostSummary, 'title' | 'slug' | 'publishedAt'>[];
69
+ /**
70
+ * The author's total published post count: the posts list `count` for
71
+ * `authorSlug`, not the length of one page.
72
+ */
58
73
  totalPosts: number;
59
74
  }): JsonLdObject | null;
60
75
 
package/dist/seo.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-Dp2jAVVM.js';
2
- import { e as JsonLdObject } from './breadcrumbs-BOFeIYEX.js';
3
- export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-BOFeIYEX.js';
4
- import './_spec-D1Qg_Twd.js';
5
- import './board-BeNZLOYC.js';
6
- import './jobs-D04ngjCP.js';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-DuNJLfIh.js';
2
+ import { e as JsonLdObject } from './breadcrumbs-DYGo0yB9.js';
3
+ export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-DYGo0yB9.js';
4
+ import './_spec-CIufstif.js';
5
+ import './board-iCHYuNhr.js';
6
+ import './jobs-DM_QMccX.js';
7
7
 
8
8
  /**
9
9
  * Blog structured data on the `@cavuno/board` wire types, transcribed from
@@ -28,13 +28,24 @@ type ArticleJsonLdPost = Pick<PublicBlogPostSummary, 'title' | 'customExcerpt' |
28
28
  * first, then `ogImageUrl` — pass your page's OG image URL (hosted passes
29
29
  * its generated `/blog/:slug/og` card; OG generation itself is app-owned
30
30
  * per ).
31
+ *
32
+ * Pass `authorProfileUrl` (the first author's profile page URL) so the
33
+ * `author` is the same `Person` (`@id` + `url`) that
34
+ * `createAuthorProfileJsonLd` declares on that page. Without it the author
35
+ * carries only a name and their website, and nothing links the post to the
36
+ * profile.
31
37
  */
32
- declare function createBlogArticleJsonLd({ post, boardName, permalink, ogImageUrl, }: {
38
+ declare function createBlogArticleJsonLd({ post, boardName, permalink, ogImageUrl, authorProfileUrl, }: {
33
39
  post: ArticleJsonLdPost;
34
40
  boardName: string;
35
41
  permalink: string;
36
42
  /** Fallback image when the post has no cover, e.g. your generated OG card URL. */
37
43
  ogImageUrl?: string | null;
44
+ /**
45
+ * Absolute canonical URL of the first author's profile page: the
46
+ * `canonical` you pass to `createAuthorProfileJsonLd` on that page.
47
+ */
48
+ authorProfileUrl?: string | null;
38
49
  }): JsonLdObject;
39
50
  /**
40
51
  * `ProfilePage` for a blog author page — the hosted shape: a `Person` main
@@ -55,6 +66,10 @@ declare function createAuthorProfileJsonLd({ author, canonical, description, ori
55
66
  origin: string | null;
56
67
  /** The author's recent posts (newest first) — the builder keeps the first 5. */
57
68
  posts: Pick<PublicBlogPostSummary, 'title' | 'slug' | 'publishedAt'>[];
69
+ /**
70
+ * The author's total published post count: the posts list `count` for
71
+ * `authorSlug`, not the length of one page.
72
+ */
58
73
  totalPosts: number;
59
74
  }): JsonLdObject | null;
60
75