@vantageos/mcp-boilerplate 0.7.0 → 0.9.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.
@@ -31,13 +31,14 @@
31
31
  import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
32
32
  import { McpServer } from "@modelcontextprotocol/server";
33
33
  import { z } from "zod";
34
- import { getBuildIdentity } from "./buildIdentity.js";
34
+ import { getBuildCommit, getBuildIdentity } from "./buildIdentity.js";
35
35
  import { t } from "./i18n.js";
36
36
  import { EXAMPLE_ITEMS_READER, LAKE_UNREADABLE_CODE, LakeReadError, } from "./lake/reader.js";
37
37
  import { rejectCursorOnSinglePageLists } from "./listCursor.js";
38
38
  import { INVALID_CURSOR_CODE, InvalidCursorError, PAGINATION, readPage } from "./pagination.js";
39
39
  import { RATE_LIMITED_CODE, toolRateLimiter } from "./rateLimit.js";
40
40
  import { trackToolRecords } from "./servedToolRecords.js";
41
+ import { getServerIdentity, serverInstructions, serverPresentation } from "./serverIdentity.js";
41
42
  import { loadServerManifest } from "./serverManifest.js";
42
43
  import { scopeRowsForTenant } from "./tenancy.js";
43
44
  import { LIST_ITEMS_ANNOTATIONS, PING_ANNOTATIONS, WHOAMI_ANNOTATIONS } from "./toolRecord.js";
@@ -120,18 +121,26 @@ export function createBoilerplateMcpServer(deps = {}) {
120
121
  // at registration and checked against it (and the closed mechanism set) before the server is
121
122
  // returned: the check covers what is served, there is no list to keep in step.
122
123
  const manifest = loadServerManifest();
123
- // The identity the server announces: version (declared) AND buildId (derived from the served
124
- // tree, src/buildIdentity.ts). The SDK sends `serverInfo` as given, on `server/discover`
125
- // (2026-07-28, `_meta["io.modelcontextprotocol/serverInfo"]`) and on `initialize` (2025-era).
126
- // `buildId` is not a modelled `Implementation` key, hence the spread (no excess-key check).
124
+ // The identity the server announces (S-49): name, version, title, description (in the caller's
125
+ // locale), websiteUrl and icons, all from src/serverIdentity.ts (package.json or environment),
126
+ // plus the build identity (C-25): buildId = the git commit the tree was built from, buildHash =
127
+ // the hash of the files this process runs from (src/buildIdentity.ts). The SDK sends
128
+ // `serverInfo` as given, on `server/discover` (2026-07-28,
129
+ // `_meta["io.modelcontextprotocol/serverInfo"]`) and on `initialize` (2025-era). The build keys
130
+ // are not modelled `Implementation` keys, hence the spread (no excess-key check).
127
131
  const server = new McpServer({
128
132
  name: SERVER_NAME,
129
133
  version: SERVER_VERSION,
130
- ...{ buildId: getBuildIdentity() },
134
+ ...serverPresentation(getServerIdentity(), locale, deps.baseUrl),
135
+ ...{
136
+ buildId: getBuildCommit(),
137
+ buildHash: getBuildIdentity(),
138
+ },
131
139
  }, {
132
140
  // The capabilities come from the server manifest (mcp-server.json, C-18): one source.
133
141
  capabilities: manifest.capabilities,
134
- instructions: "Clonable MCP server boilerplate — see README.md",
142
+ // How an agent uses this server, in the caller's locale (src/serverIdentity.ts).
143
+ instructions: serverInstructions(locale),
135
144
  supportedProtocolVersions: SUPPORTED_LEGACY_PROTOCOL_VERSIONS,
136
145
  cacheHints: {
137
146
  "tools/list": { ttlMs: 60_000, cacheScope: "public" },
@@ -302,5 +311,17 @@ export function createBoilerplateMcpServer(deps = {}) {
302
311
  rejectCursorOnSinglePageLists(server);
303
312
  // Start refusal: every registered tool's record, as registered, must be sound.
304
313
  served.assertServed(manifest.contextCeiling);
314
+ SUMMARIES.set(server, served.summaries);
305
315
  return server;
306
316
  }
317
+ const SUMMARIES = new WeakMap();
318
+ /**
319
+ * The tools a server built by this factory registered (name, title, description), for the public
320
+ * server card (src/serverCard.ts): read from the registrations, never a second list.
321
+ */
322
+ export function registeredToolSummaries(server) {
323
+ const summaries = SUMMARIES.get(server);
324
+ if (!summaries)
325
+ throw new Error("registeredToolSummaries: not a server built by this factory");
326
+ return summaries;
327
+ }
@@ -12,6 +12,12 @@ import type { ContextCeiling } from "./serverManifest.js";
12
12
  export interface ServedToolRecords {
13
13
  /** The annotations of every tool registered so far, by tool name. */
14
14
  readonly records: Readonly<Record<string, Record<string, unknown>>>;
15
+ /** Each registered tool's name, title and description, in registration order (the server card). */
16
+ readonly summaries: ReadonlyArray<{
17
+ name: string;
18
+ title?: string;
19
+ description?: string;
20
+ }>;
15
21
  /** Throws `ToolRecordError` naming every unsound tool; same check as the wire test. */
16
22
  assertServed(ceiling: ContextCeiling): void;
17
23
  }
@@ -1,10 +1,20 @@
1
1
  import { assertToolRecords } from "./toolRecord.js";
2
2
  export function trackToolRecords(server) {
3
3
  const records = {};
4
+ const summaries = [];
4
5
  const register = server.registerTool.bind(server);
5
6
  server.registerTool = ((name, config, callback) => {
6
7
  records[name] = { ...(config.annotations ?? {}) };
8
+ summaries.push({
9
+ name,
10
+ ...(config.title !== undefined ? { title: config.title } : {}),
11
+ ...(config.description !== undefined ? { description: config.description } : {}),
12
+ });
7
13
  return register(name, config, callback);
8
14
  });
9
- return { records, assertServed: (ceiling) => assertToolRecords(records, ceiling) };
15
+ return {
16
+ records,
17
+ summaries,
18
+ assertServed: (ceiling) => assertToolRecords(records, ceiling),
19
+ };
10
20
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The public MCP Server Card (mcp-server-standard S-50, Manufact "mcp.json or MCP Server Card
3
+ * served"). Served by src/httpApp.ts, before every gate, at each path in `SERVER_CARD_PATHS`.
4
+ *
5
+ * Shape: the `ServerCard` of the MCP Server Card extension (SEP-2127,
6
+ * modelcontextprotocol/experimental-ext-server-card `schema.json` at commit
7
+ * 526201bbc80231daa40ffcdecfc9da4e54e5dc93, sha256
8
+ * 2c772b51edb367f154771d84ddbae87ddba00a624422c8e46f218a9ac03bf042), vendored at
9
+ * tests/fixtures/server-card.schema.json and validated in tests/serverMetadata.test.ts.
10
+ *
11
+ * Declared divergence: the extension says a card does not enumerate primitives, while Manufact's
12
+ * check and mcp-doctor S-50 read `capabilities` and a tool summary (`tools`) on the card. The
13
+ * schema allows additional members, so the card carries both: the capabilities of
14
+ * mcp-server.json (the same object `initialize` serves) and each served tool's name, title and
15
+ * description, read from the tools as registered — never a second list to keep in step.
16
+ *
17
+ * The card is public metadata and grants nothing. Every string comes from src/serverIdentity.ts
18
+ * (the source of the serverInfo), and the URLs from the origin the server is reached at.
19
+ */
20
+ import type { Icon } from "@modelcontextprotocol/server";
21
+ import { type ServerIdentity } from "./serverIdentity.js";
22
+ export declare const SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
23
+ export declare const SERVER_CARD_MEDIA_TYPE = "application/mcp-server-card+json";
24
+ /**
25
+ * Where the card is served: `/mcp/server-card` is the location the extension reserves
26
+ * (`<streamable-http-url>/server-card`); the others are the origin-level paths Manufact and
27
+ * mcp-doctor S-50 probe. Manufact also reads `/.well-known/ai-catalog.json` and the parent
28
+ * domain; an AI Catalog lists cards rather than being one, so it is not served here.
29
+ */
30
+ export declare const SERVER_CARD_PATHS: readonly ["/mcp/server-card", "/.well-known/mcp/server-card.json", "/.well-known/mcp-server-card", "/mcp.json", "/.well-known/mcp.json"];
31
+ /** One served tool, as summarised on the card. */
32
+ export interface CardTool {
33
+ name: string;
34
+ title?: string;
35
+ description?: string;
36
+ }
37
+ export interface ServerCard {
38
+ $schema: string;
39
+ name: string;
40
+ version: string;
41
+ description: string;
42
+ title: string;
43
+ websiteUrl: string;
44
+ icons: Icon[];
45
+ remotes: Array<{
46
+ type: "streamable-http";
47
+ url: string;
48
+ }>;
49
+ capabilities: Record<string, unknown>;
50
+ tools: CardTool[];
51
+ }
52
+ export interface CardInput {
53
+ identity: ServerIdentity;
54
+ /** The served MCP endpoint URL. */
55
+ mcpUrl: string;
56
+ /** The base URL the icon is served under (ends in `/`). */
57
+ baseUrl: string;
58
+ capabilities: Record<string, unknown>;
59
+ tools: CardTool[];
60
+ }
61
+ export declare function buildServerCard(input: CardInput): ServerCard;
62
+ /**
63
+ * The card as an HTTP response: the card media type when the client asks for it, JSON otherwise.
64
+ * `cacheable` is false when the origin was read from the
65
+ * request (no configured public base URL): a shared cache must not keep a card naming a Host
66
+ * header another client sent.
67
+ */
68
+ export declare function serverCardResponse(req: Request, card: ServerCard, cacheable: boolean): Response;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The public MCP Server Card (mcp-server-standard S-50, Manufact "mcp.json or MCP Server Card
3
+ * served"). Served by src/httpApp.ts, before every gate, at each path in `SERVER_CARD_PATHS`.
4
+ *
5
+ * Shape: the `ServerCard` of the MCP Server Card extension (SEP-2127,
6
+ * modelcontextprotocol/experimental-ext-server-card `schema.json` at commit
7
+ * 526201bbc80231daa40ffcdecfc9da4e54e5dc93, sha256
8
+ * 2c772b51edb367f154771d84ddbae87ddba00a624422c8e46f218a9ac03bf042), vendored at
9
+ * tests/fixtures/server-card.schema.json and validated in tests/serverMetadata.test.ts.
10
+ *
11
+ * Declared divergence: the extension says a card does not enumerate primitives, while Manufact's
12
+ * check and mcp-doctor S-50 read `capabilities` and a tool summary (`tools`) on the card. The
13
+ * schema allows additional members, so the card carries both: the capabilities of
14
+ * mcp-server.json (the same object `initialize` serves) and each served tool's name, title and
15
+ * description, read from the tools as registered — never a second list to keep in step.
16
+ *
17
+ * The card is public metadata and grants nothing. Every string comes from src/serverIdentity.ts
18
+ * (the source of the serverInfo), and the URLs from the origin the server is reached at.
19
+ */
20
+ import { cardDescription, cardName, serverIcons } from "./serverIdentity.js";
21
+ export const SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
22
+ export const SERVER_CARD_MEDIA_TYPE = "application/mcp-server-card+json";
23
+ /**
24
+ * Where the card is served: `/mcp/server-card` is the location the extension reserves
25
+ * (`<streamable-http-url>/server-card`); the others are the origin-level paths Manufact and
26
+ * mcp-doctor S-50 probe. Manufact also reads `/.well-known/ai-catalog.json` and the parent
27
+ * domain; an AI Catalog lists cards rather than being one, so it is not served here.
28
+ */
29
+ export const SERVER_CARD_PATHS = [
30
+ "/mcp/server-card",
31
+ "/.well-known/mcp/server-card.json",
32
+ "/.well-known/mcp-server-card",
33
+ "/mcp.json",
34
+ "/.well-known/mcp.json",
35
+ ];
36
+ export function buildServerCard(input) {
37
+ const { identity } = input;
38
+ return {
39
+ $schema: SERVER_CARD_SCHEMA_URL,
40
+ name: cardName(identity.websiteUrl, identity.name),
41
+ version: identity.version,
42
+ description: cardDescription(identity.description.en),
43
+ title: identity.title,
44
+ websiteUrl: identity.websiteUrl,
45
+ icons: serverIcons(input.baseUrl),
46
+ remotes: [{ type: "streamable-http", url: input.mcpUrl }],
47
+ capabilities: input.capabilities,
48
+ tools: input.tools,
49
+ };
50
+ }
51
+ /**
52
+ * The card as an HTTP response: the card media type when the client asks for it, JSON otherwise.
53
+ * `cacheable` is false when the origin was read from the
54
+ * request (no configured public base URL): a shared cache must not keep a card naming a Host
55
+ * header another client sent.
56
+ */
57
+ export function serverCardResponse(req, card, cacheable) {
58
+ const accept = req.headers.get("accept") ?? "";
59
+ const headers = new Headers({
60
+ "content-type": accept.includes(SERVER_CARD_MEDIA_TYPE)
61
+ ? SERVER_CARD_MEDIA_TYPE
62
+ : "application/json",
63
+ "cache-control": cacheable ? "public, max-age=3600" : "no-store",
64
+ "access-control-allow-origin": "*",
65
+ "x-content-type-options": "nosniff",
66
+ });
67
+ return new Response(JSON.stringify(card), { status: 200, headers });
68
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The server icon (MCP `Implementation.icons`, Manufact "Scalable or high-resolution icon"). A
3
+ * vector, so it is sharp at every size a host renders and needs no `sizes` beyond `"any"`.
4
+ *
5
+ * A fork replaces `SERVER_ICON_SVG` with its own mark. Keep it a self-contained SVG: no script,
6
+ * no external reference (the route serves it under a CSP that forbids both), because a host may
7
+ * render it in its own page.
8
+ *
9
+ * Served by src/httpApp.ts at `SERVER_ICON_PATH`, before any authentication, and inlined as a
10
+ * `data:` URI where there is no HTTP origin (stdio).
11
+ */
12
+ /** Where the HTTP transport serves the icon, relative to the public base URL. */
13
+ export declare const SERVER_ICON_PATH = "icon.svg";
14
+ export declare const SERVER_ICON_MIME_TYPE = "image/svg+xml";
15
+ export declare const SERVER_ICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\" role=\"img\" aria-label=\"MCP server\">\n<rect width=\"128\" height=\"128\" rx=\"28\" fill=\"#1f2a44\"/>\n<path d=\"M40 44 L88 44 M40 44 L64 88 M88 44 L64 88\" stroke=\"#8fb3ff\" stroke-width=\"8\" stroke-linecap=\"round\" fill=\"none\"/>\n<circle cx=\"40\" cy=\"44\" r=\"12\" fill=\"#ffffff\"/>\n<circle cx=\"88\" cy=\"44\" r=\"12\" fill=\"#ffffff\"/>\n<circle cx=\"64\" cy=\"88\" r=\"12\" fill=\"#ffffff\"/>\n</svg>\n";
16
+ /** The icon as a `data:` URI, for a transport with no HTTP origin to serve it from. */
17
+ export declare function serverIconDataUri(): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The server icon (MCP `Implementation.icons`, Manufact "Scalable or high-resolution icon"). A
3
+ * vector, so it is sharp at every size a host renders and needs no `sizes` beyond `"any"`.
4
+ *
5
+ * A fork replaces `SERVER_ICON_SVG` with its own mark. Keep it a self-contained SVG: no script,
6
+ * no external reference (the route serves it under a CSP that forbids both), because a host may
7
+ * render it in its own page.
8
+ *
9
+ * Served by src/httpApp.ts at `SERVER_ICON_PATH`, before any authentication, and inlined as a
10
+ * `data:` URI where there is no HTTP origin (stdio).
11
+ */
12
+ /** Where the HTTP transport serves the icon, relative to the public base URL. */
13
+ export const SERVER_ICON_PATH = "icon.svg";
14
+ export const SERVER_ICON_MIME_TYPE = "image/svg+xml";
15
+ export const SERVER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="MCP server">
16
+ <rect width="128" height="128" rx="28" fill="#1f2a44"/>
17
+ <path d="M40 44 L88 44 M40 44 L64 88 M88 44 L64 88" stroke="#8fb3ff" stroke-width="8" stroke-linecap="round" fill="none"/>
18
+ <circle cx="40" cy="44" r="12" fill="#ffffff"/>
19
+ <circle cx="88" cy="44" r="12" fill="#ffffff"/>
20
+ <circle cx="64" cy="88" r="12" fill="#ffffff"/>
21
+ </svg>
22
+ `;
23
+ /** The icon as a `data:` URI, for a transport with no HTTP origin to serve it from. */
24
+ export function serverIconDataUri() {
25
+ return `data:${SERVER_ICON_MIME_TYPE};base64,${Buffer.from(SERVER_ICON_SVG, "utf8").toString("base64")}`;
26
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The server's public identity: the ONE source of every string a host reads about this server
3
+ * before or at connection — the `serverInfo` of `initialize` / `server/discover`
4
+ * (src/mcpServerFactory.ts), its `instructions`, the MCP Server Card (src/serverCard.ts) and the
5
+ * setup page at the MCP URL (src/landingPage.ts). No copy can drift from another.
6
+ *
7
+ * Every value is DERIVED, never a literal a fork must remember to edit:
8
+ *
9
+ * | Item | Environment override | Otherwise, package.json |
10
+ * |----------------|-------------------------------|-------------------------|
11
+ * | title | `MCP_SERVER_TITLE` | `displayName` |
12
+ * | description EN | `MCP_SERVER_DESCRIPTION` | `description` |
13
+ * | description FR | `MCP_SERVER_DESCRIPTION_FR` | `description_fr` |
14
+ * | website | `MCP_SERVER_WEBSITE_URL` | `homepage` |
15
+ *
16
+ * `name` and `version` keep their own sources (src/version.ts). The template's package.json
17
+ * carries the template's values; a fork edits package.json (or sets the variables) and every
18
+ * surface follows. A value neither source carries is refused at start, naming the variable AND
19
+ * the package.json field, in English and French: a served placeholder is what Manufact and
20
+ * mcp-doctor S-49 score as missing, so the server never invents one.
21
+ */
22
+ import type { Icon, Implementation } from "@modelcontextprotocol/server";
23
+ import { type McpLocale } from "./i18n.js";
24
+ /** The package.json fields the identity reads. */
25
+ export interface PackageIdentityFields {
26
+ displayName?: unknown;
27
+ description?: unknown;
28
+ description_fr?: unknown;
29
+ homepage?: unknown;
30
+ }
31
+ export interface ServerIdentity {
32
+ name: string;
33
+ version: string;
34
+ title: string;
35
+ description: Record<McpLocale, string>;
36
+ websiteUrl: string;
37
+ }
38
+ /** The environment variables, by identity item (documented in README and .env.example). */
39
+ export declare const IDENTITY_ENV: {
40
+ readonly title: "MCP_SERVER_TITLE";
41
+ readonly description: "MCP_SERVER_DESCRIPTION";
42
+ readonly descriptionFr: "MCP_SERVER_DESCRIPTION_FR";
43
+ readonly websiteUrl: "MCP_SERVER_WEBSITE_URL";
44
+ };
45
+ /** Resolves the identity from `env` and the package.json fields. Throws, naming what is missing. */
46
+ export declare function resolveServerIdentity(env: NodeJS.ProcessEnv, pkg: PackageIdentityFields): ServerIdentity;
47
+ /**
48
+ * The identity of THIS process: the package fields embedded at build (src/embedded.generated.ts, from
49
+ * package.json; no file read, so a bundled server has them too) and process.env. Once.
50
+ */
51
+ export declare function getServerIdentity(): ServerIdentity;
52
+ /**
53
+ * The icons a host reads. With an HTTP base URL (the origin the server is reached at, ending in
54
+ * `/`), the icon is the URL src/httpApp.ts serves; without one (stdio), it travels inline.
55
+ */
56
+ export declare function serverIcons(baseUrl?: string): Icon[];
57
+ /**
58
+ * The `serverInfo` (MCP `Implementation`) items beyond name and version, for a caller's locale.
59
+ * The factory writes `name` and `version` beside them from src/version.ts, where the
60
+ * version-observability check (D16) reads them.
61
+ */
62
+ export declare function serverPresentation(identity: ServerIdentity, locale: McpLocale, baseUrl?: string): Omit<Implementation, "name" | "version">;
63
+ /**
64
+ * The server instructions (`InitializeResult.instructions` / `DiscoverResult.instructions`): how
65
+ * an agent should use THIS server, critical guidance first, from the locale files
66
+ * (`Mcp.server.instructions`). A fork rewrites them there for its own tools;
67
+ * tests/serverMetadata.test.ts fails when a served tool goes unnamed or the text passes Claude
68
+ * Code's 2048-byte field budget.
69
+ */
70
+ export declare function serverInstructions(locale: McpLocale): string;
71
+ /**
72
+ * The card's reverse-DNS name, derived from the website: `io.github.<owner>/<name>` for a GitHub
73
+ * website (the MCP Registry convention), the reversed host otherwise (`www.` dropped).
74
+ */
75
+ export declare function cardName(websiteUrl: string, serverName: string): string;
76
+ /** The card schema's `description` cap. */
77
+ export declare const CARD_DESCRIPTION_MAX = 100;
78
+ /**
79
+ * The card description: the English description whole when it fits the schema's 100 characters;
80
+ * otherwise cut on the last word boundary that leaves room for an ellipsis. Declared divergence:
81
+ * the card then carries a shortened text, the serverInfo the full one.
82
+ */
83
+ export declare function cardDescription(description: string): string;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The server's public identity: the ONE source of every string a host reads about this server
3
+ * before or at connection — the `serverInfo` of `initialize` / `server/discover`
4
+ * (src/mcpServerFactory.ts), its `instructions`, the MCP Server Card (src/serverCard.ts) and the
5
+ * setup page at the MCP URL (src/landingPage.ts). No copy can drift from another.
6
+ *
7
+ * Every value is DERIVED, never a literal a fork must remember to edit:
8
+ *
9
+ * | Item | Environment override | Otherwise, package.json |
10
+ * |----------------|-------------------------------|-------------------------|
11
+ * | title | `MCP_SERVER_TITLE` | `displayName` |
12
+ * | description EN | `MCP_SERVER_DESCRIPTION` | `description` |
13
+ * | description FR | `MCP_SERVER_DESCRIPTION_FR` | `description_fr` |
14
+ * | website | `MCP_SERVER_WEBSITE_URL` | `homepage` |
15
+ *
16
+ * `name` and `version` keep their own sources (src/version.ts). The template's package.json
17
+ * carries the template's values; a fork edits package.json (or sets the variables) and every
18
+ * surface follows. A value neither source carries is refused at start, naming the variable AND
19
+ * the package.json field, in English and French: a served placeholder is what Manufact and
20
+ * mcp-doctor S-49 score as missing, so the server never invents one.
21
+ */
22
+ import { isSecureUrl } from "./declaredVariables.js";
23
+ import { EMBEDDED_PACKAGE_IDENTITY } from "./embedded.generated.js";
24
+ import { t, tBoth } from "./i18n.js";
25
+ import { SERVER_ICON_MIME_TYPE, SERVER_ICON_PATH, serverIconDataUri } from "./serverIcon.js";
26
+ import { SERVER_NAME, SERVER_VERSION } from "./version.js";
27
+ /** The environment variables, by identity item (documented in README and .env.example). */
28
+ export const IDENTITY_ENV = {
29
+ title: "MCP_SERVER_TITLE",
30
+ description: "MCP_SERVER_DESCRIPTION",
31
+ descriptionFr: "MCP_SERVER_DESCRIPTION_FR",
32
+ websiteUrl: "MCP_SERVER_WEBSITE_URL",
33
+ };
34
+ function pick(env, pkg, item, variable, field) {
35
+ const fromEnv = env[variable];
36
+ if (fromEnv !== undefined && fromEnv.trim() !== "")
37
+ return { value: fromEnv, source: variable };
38
+ const fromPkg = pkg[field];
39
+ if (typeof fromPkg === "string" && fromPkg.trim() !== "")
40
+ return { value: fromPkg, source: `package.json ${field}` };
41
+ throw new Error(tBoth("server.identityMissing", { item, variable, field }));
42
+ }
43
+ /** Resolves the identity from `env` and the package.json fields. Throws, naming what is missing. */
44
+ export function resolveServerIdentity(env, pkg) {
45
+ const title = pick(env, pkg, "title", IDENTITY_ENV.title, "displayName");
46
+ const en = pick(env, pkg, "description", IDENTITY_ENV.description, "description");
47
+ const fr = pick(env, pkg, "description_fr", IDENTITY_ENV.descriptionFr, "description_fr");
48
+ const site = pick(env, pkg, "websiteUrl", IDENTITY_ENV.websiteUrl, "homepage");
49
+ if (!isSecureUrl(site.value))
50
+ throw new Error(tBoth("server.identityInvalid", { item: "websiteUrl", source: site.source }));
51
+ return {
52
+ name: SERVER_NAME,
53
+ version: SERVER_VERSION,
54
+ title: title.value,
55
+ description: { en: en.value, fr: fr.value },
56
+ websiteUrl: site.value,
57
+ };
58
+ }
59
+ let processIdentity;
60
+ /**
61
+ * The identity of THIS process: the package fields embedded at build (src/embedded.generated.ts, from
62
+ * package.json; no file read, so a bundled server has them too) and process.env. Once.
63
+ */
64
+ export function getServerIdentity() {
65
+ processIdentity ??= resolveServerIdentity(process.env, EMBEDDED_PACKAGE_IDENTITY);
66
+ return processIdentity;
67
+ }
68
+ /**
69
+ * The icons a host reads. With an HTTP base URL (the origin the server is reached at, ending in
70
+ * `/`), the icon is the URL src/httpApp.ts serves; without one (stdio), it travels inline.
71
+ */
72
+ export function serverIcons(baseUrl) {
73
+ const src = baseUrl ? new URL(SERVER_ICON_PATH, baseUrl).href : serverIconDataUri();
74
+ return [{ src, mimeType: SERVER_ICON_MIME_TYPE, sizes: ["any"] }];
75
+ }
76
+ /**
77
+ * The `serverInfo` (MCP `Implementation`) items beyond name and version, for a caller's locale.
78
+ * The factory writes `name` and `version` beside them from src/version.ts, where the
79
+ * version-observability check (D16) reads them.
80
+ */
81
+ export function serverPresentation(identity, locale, baseUrl) {
82
+ return {
83
+ title: identity.title,
84
+ description: identity.description[locale],
85
+ websiteUrl: identity.websiteUrl,
86
+ icons: serverIcons(baseUrl),
87
+ };
88
+ }
89
+ /**
90
+ * The server instructions (`InitializeResult.instructions` / `DiscoverResult.instructions`): how
91
+ * an agent should use THIS server, critical guidance first, from the locale files
92
+ * (`Mcp.server.instructions`). A fork rewrites them there for its own tools;
93
+ * tests/serverMetadata.test.ts fails when a served tool goes unnamed or the text passes Claude
94
+ * Code's 2048-byte field budget.
95
+ */
96
+ export function serverInstructions(locale) {
97
+ return t(locale, "server.instructions");
98
+ }
99
+ /** The Server Card `name` pattern (experimental-ext-server-card schema). */
100
+ const CARD_NAME = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
101
+ /**
102
+ * The card's reverse-DNS name, derived from the website: `io.github.<owner>/<name>` for a GitHub
103
+ * website (the MCP Registry convention), the reversed host otherwise (`www.` dropped).
104
+ */
105
+ export function cardName(websiteUrl, serverName) {
106
+ const url = new URL(websiteUrl);
107
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
108
+ const owner = url.pathname.split("/").filter(Boolean)[0];
109
+ const namespace = host === "github.com" && owner
110
+ ? `io.github.${owner.toLowerCase()}`
111
+ : host.split(".").reverse().join(".");
112
+ const name = `${namespace}/${serverName}`;
113
+ if (!CARD_NAME.test(name))
114
+ throw new Error(`server card name ${JSON.stringify(name)} does not match ${CARD_NAME}`);
115
+ return name;
116
+ }
117
+ /** The card schema's `description` cap. */
118
+ export const CARD_DESCRIPTION_MAX = 100;
119
+ /**
120
+ * The card description: the English description whole when it fits the schema's 100 characters;
121
+ * otherwise cut on the last word boundary that leaves room for an ellipsis. Declared divergence:
122
+ * the card then carries a shortened text, the serverInfo the full one.
123
+ */
124
+ export function cardDescription(description) {
125
+ if (description.length <= CARD_DESCRIPTION_MAX)
126
+ return description;
127
+ const room = description.slice(0, CARD_DESCRIPTION_MAX - 1);
128
+ const cut = room.lastIndexOf(" ");
129
+ return `${(cut > 0 ? room.slice(0, cut) : room).trimEnd()}…`;
130
+ }
@@ -21,6 +21,12 @@ export declare function buildItemsUiResourceMeta(domain?: string): McpUiResource
21
21
  * The widget template. It carries no row: the widget renders the `list_items` RESULT it is given,
22
22
  * so the template is the same for every caller and every page.
23
23
  */
24
+ /**
25
+ * What the widget shows, for the host and the model, in both languages ("EN / FR"). Hosts read it
26
+ * under two keys: `_meta.ui.widgetDescription` and the ChatGPT hint `_meta["openai/widgetDescription"]`
27
+ * (Manufact's tool-resource-metadata-complete reads exactly these); the same text under both.
28
+ */
29
+ export declare const WIDGET_DESCRIPTION: string;
24
30
  export declare function buildUiResource(props: {
25
31
  domain?: string;
26
32
  }): {
@@ -28,7 +34,14 @@ export declare function buildUiResource(props: {
28
34
  mimeType: string;
29
35
  text: string;
30
36
  _meta: {
31
- ui: McpUiResourceMeta;
37
+ ui: {
38
+ widgetDescription: string;
39
+ csp?: import("@modelcontextprotocol/ext-apps").McpUiResourceCsp;
40
+ permissions?: import("@modelcontextprotocol/ext-apps").McpUiResourcePermissions;
41
+ domain?: string;
42
+ prefersBorder?: boolean;
43
+ };
44
+ "openai/widgetDescription": string;
32
45
  };
33
46
  };
34
47
  /**
@@ -13,6 +13,7 @@
13
13
  import { readFileSync } from "node:fs";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
16
+ import { tBoth } from "../i18n.js";
16
17
  export const ITEMS_UI_RESOURCE_URI = "ui://mcp-boilerplate/items-widget.html";
17
18
  // dist/ui-bundle/ — NEVER dist/ui/ (that path is tsc's compiled-server
18
19
  // output for this very module; see vite.config.ts for the collision this
@@ -69,12 +70,21 @@ export function buildItemsUiResourceMeta(domain) {
69
70
  * The widget template. It carries no row: the widget renders the `list_items` RESULT it is given,
70
71
  * so the template is the same for every caller and every page.
71
72
  */
73
+ /**
74
+ * What the widget shows, for the host and the model, in both languages ("EN / FR"). Hosts read it
75
+ * under two keys: `_meta.ui.widgetDescription` and the ChatGPT hint `_meta["openai/widgetDescription"]`
76
+ * (Manufact's tool-resource-metadata-complete reads exactly these); the same text under both.
77
+ */
78
+ export const WIDGET_DESCRIPTION = tBoth("widget.description");
72
79
  export function buildUiResource(props) {
73
80
  return {
74
81
  uri: ITEMS_UI_RESOURCE_URI,
75
82
  mimeType: RESOURCE_MIME_TYPE,
76
83
  text: readUiBundle(),
77
- _meta: { ui: buildItemsUiResourceMeta(props.domain) },
84
+ _meta: {
85
+ ui: { ...buildItemsUiResourceMeta(props.domain), widgetDescription: WIDGET_DESCRIPTION },
86
+ "openai/widgetDescription": WIDGET_DESCRIPTION,
87
+ },
78
88
  };
79
89
  }
80
90
  /**
package/dist/version.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The advertised server version is DERIVED from package.json (never a literal).
3
- * `src/version.ts` and `dist/version.js` both sit one level under the package root,
4
- * so the same relative URL resolves in source mode (vitest) and in the built output.
2
+ * The advertised server version is DERIVED from package.json (never a literal): scripts/embed-static.mjs writes
3
+ * it into src/embedded.generated.ts at build, and `--check` (tests/embedded-static.test.ts) refuses a stale copy.
4
+ * It is a module value, not a file read at import: a bundled consumer carries no package.json beside this module
5
+ * (task k17aj33ks3ndyxgt7jch68rpwx8hz9x5).
5
6
  */
6
- import { readFileSync } from "node:fs";
7
+ import { EMBEDDED_PACKAGE_VERSION } from "./embedded.generated.js";
7
8
  export const SERVER_NAME = "mcp-boilerplate";
8
- export const SERVER_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
9
+ export const SERVER_VERSION = EMBEDDED_PACKAGE_VERSION;