@vantageos/mcp-boilerplate 0.6.3 → 0.8.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 (70) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +39 -0
  3. package/README.fr.md +146 -10
  4. package/README.md +141 -12
  5. package/dist/build-info.json +1 -0
  6. package/dist/buildIdentity.d.ts +16 -0
  7. package/dist/buildIdentity.js +39 -0
  8. package/dist/embedded.generated.d.ts +225 -0
  9. package/dist/embedded.generated.js +8 -0
  10. package/dist/hostVerification.d.ts +15 -0
  11. package/dist/hostVerification.js +32 -0
  12. package/dist/http.js +9 -4
  13. package/dist/httpApp.d.ts +15 -0
  14. package/dist/httpApp.js +119 -9
  15. package/dist/i18n.js +6 -16
  16. package/dist/landingPage.d.ts +24 -0
  17. package/dist/landingPage.js +109 -0
  18. package/dist/listCursor.d.ts +10 -0
  19. package/dist/listCursor.js +37 -0
  20. package/dist/mcpServerFactory.d.ts +22 -2
  21. package/dist/mcpServerFactory.js +125 -22
  22. package/dist/oauth/authorizationServer.js +33 -12
  23. package/dist/oauth/picker.js +1 -0
  24. package/dist/oauth/settings.d.ts +2 -0
  25. package/dist/oauth/settings.js +2 -0
  26. package/dist/oauth/tokenIssuer.d.ts +0 -1
  27. package/dist/oauth/tokenIssuer.js +1 -18
  28. package/dist/pagination.d.ts +2 -1
  29. package/dist/pagination.js +2 -1
  30. package/dist/rateLimit.d.ts +33 -0
  31. package/dist/rateLimit.js +48 -0
  32. package/dist/servedToolRecords.d.ts +6 -0
  33. package/dist/servedToolRecords.js +11 -1
  34. package/dist/serverCard.d.ts +68 -0
  35. package/dist/serverCard.js +68 -0
  36. package/dist/serverIcon.d.ts +17 -0
  37. package/dist/serverIcon.js +26 -0
  38. package/dist/serverIdentity.d.ts +83 -0
  39. package/dist/serverIdentity.js +130 -0
  40. package/dist/serverManifest.d.ts +9 -1
  41. package/dist/serverManifest.js +10 -1
  42. package/dist/signedCursor.js +6 -1
  43. package/dist/stdio.js +2 -2
  44. package/dist/tenancy.d.ts +4 -14
  45. package/dist/tenancy.js +7 -18
  46. package/dist/toolRecord.d.ts +35 -2
  47. package/dist/toolRecord.js +44 -2
  48. package/dist/toolRoleGate.d.ts +41 -0
  49. package/dist/toolRoleGate.js +94 -0
  50. package/dist/toolRoles.d.ts +57 -0
  51. package/dist/toolRoles.js +113 -0
  52. package/dist/ui/sandboxDomain.d.ts +13 -0
  53. package/dist/ui/sandboxDomain.js +23 -0
  54. package/dist/ui/uiResource.d.ts +14 -1
  55. package/dist/ui/uiResource.js +11 -1
  56. package/dist/upstream/paginatedClient.js +1 -0
  57. package/dist/verifiedPerson.d.ts +6 -0
  58. package/dist/verifiedPerson.js +9 -0
  59. package/dist/version.js +6 -5
  60. package/docs/manufact-gap.md +200 -0
  61. package/docs/releasing.md +19 -0
  62. package/mcp-server.json +3 -2
  63. package/package.json +11 -3
  64. package/plugin.json +2 -4
  65. package/scripts/plugin-manifest.source.json +19 -0
  66. package/scripts/readme-fr.mjs +11 -4
  67. package/dist/locales/en.json +0 -77
  68. package/dist/locales/fr.json +0 -77
  69. package/dist/oauth/clientAssertion.d.ts +0 -28
  70. package/dist/oauth/clientAssertion.js +0 -106
@@ -4,7 +4,7 @@
4
4
  * deployment's key. The resource server (@vantageos/cloud-identity `verifyMcpAccessToken`)
5
5
  * verifies the result against the public half published at /.well-known/jwks.json.
6
6
  */
7
- import { SignJWT, createLocalJWKSet, importJWK, jwtVerify } from "jose";
7
+ import { SignJWT, importJWK } from "jose";
8
8
  export const ACCESS_TOKEN_TTL_SECONDS = 3600;
9
9
  export async function createTokenIssuer(privateJwk) {
10
10
  const key = await importJWK(privateJwk, "RS256");
@@ -18,7 +18,6 @@ export async function createTokenIssuer(privateJwk) {
18
18
  use: "sig",
19
19
  };
20
20
  const jwks = { keys: [publicJwk] };
21
- const verifyKey = createLocalJWKSet(jwks);
22
21
  return {
23
22
  kid: privateJwk.kid,
24
23
  publicJwk,
@@ -41,21 +40,5 @@ export async function createTokenIssuer(privateJwk) {
41
40
  .setJti(crypto.randomUUID())
42
41
  .sign(key);
43
42
  },
44
- async verify(token, issuer, audience) {
45
- const { payload } = await jwtVerify(token, verifyKey, {
46
- issuer,
47
- audience,
48
- algorithms: ["RS256"],
49
- });
50
- return {
51
- sub: String(payload.sub),
52
- org_id: String(payload.org_id),
53
- org_slug: payload.org_slug ?? null,
54
- org_role: String(payload.org_role),
55
- aud: audience,
56
- client_id: String(payload.client_id),
57
- scope: String(payload.scope),
58
- };
59
- },
60
43
  };
61
44
  }
@@ -14,7 +14,8 @@
14
14
  * fit is exhausted, never a false truncation);
15
15
  * - an invalid or expired cursor is refused with the stable code `invalid_cursor`, never read as
16
16
  * "page one". It is a TOOL INPUT error (a result with `isError: true`, spec 2025-11-25
17
- * SEP-1303), not a JSON-RPC error: -32602 stays with the protocol list methods' cursor.
17
+ * SEP-1303), not a JSON-RPC error: -32602 stays with the protocol list methods' cursor
18
+ * (src/listCursor.ts).
18
19
  */
19
20
  import { type LakeReader, type LakeScope } from "./lake/reader.js";
20
21
  import { type CursorSigner } from "./signedCursor.js";
@@ -14,7 +14,8 @@
14
14
  * fit is exhausted, never a false truncation);
15
15
  * - an invalid or expired cursor is refused with the stable code `invalid_cursor`, never read as
16
16
  * "page one". It is a TOOL INPUT error (a result with `isError: true`, spec 2025-11-25
17
- * SEP-1303), not a JSON-RPC error: -32602 stays with the protocol list methods' cursor.
17
+ * SEP-1303), not a JSON-RPC error: -32602 stays with the protocol list methods' cursor
18
+ * (src/listCursor.ts).
18
19
  */
19
20
  import { rangeAfter } from "./exampleItems.js";
20
21
  import { checkLakePage } from "./lake/reader.js";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Tool invocation rate limiting (mcp-server-standard S-24: every published tool applies rate limiting to its
3
+ * invocation, a MUST at the rank of input validation). A token bucket per caller: `capacity` calls may burst, then
4
+ * the bucket refills at `perMinute`. The buckets live for the process, not for one request: the HTTP tier builds a
5
+ * server per request, so a per-server bucket would never fill.
6
+ *
7
+ * Configuration (environment, read once at start): TOOL_RATE_LIMIT_PER_MINUTE (default 60) and TOOL_RATE_LIMIT_BURST
8
+ * (default 30). A value that is not a positive integer refuses the start, naming the variable.
9
+ */
10
+ export type RateDecision = {
11
+ ok: true;
12
+ } | {
13
+ ok: false;
14
+ retryAfterMs: number;
15
+ };
16
+ export interface RateLimiter {
17
+ /** Takes one token from `key`'s bucket, or says how long until one is available. */
18
+ consume(key: string): RateDecision;
19
+ }
20
+ export declare function createTokenBucketLimiter(opts: {
21
+ capacity: number;
22
+ perMinute: number;
23
+ now?: () => number;
24
+ }): RateLimiter;
25
+ /** The limits read from the environment. */
26
+ export declare function loadToolRateLimit(env?: NodeJS.ProcessEnv): {
27
+ capacity: number;
28
+ perMinute: number;
29
+ };
30
+ /** The process-wide limiter every tool invocation goes through, unless a test injects its own. */
31
+ export declare const toolRateLimiter: RateLimiter;
32
+ /** The stable machine code of a refused invocation (beside a message localized per X-MCP-Locale). */
33
+ export declare const RATE_LIMITED_CODE = "rate_limited";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tool invocation rate limiting (mcp-server-standard S-24: every published tool applies rate limiting to its
3
+ * invocation, a MUST at the rank of input validation). A token bucket per caller: `capacity` calls may burst, then
4
+ * the bucket refills at `perMinute`. The buckets live for the process, not for one request: the HTTP tier builds a
5
+ * server per request, so a per-server bucket would never fill.
6
+ *
7
+ * Configuration (environment, read once at start): TOOL_RATE_LIMIT_PER_MINUTE (default 60) and TOOL_RATE_LIMIT_BURST
8
+ * (default 30). A value that is not a positive integer refuses the start, naming the variable.
9
+ */
10
+ export function createTokenBucketLimiter(opts) {
11
+ const now = opts.now ?? Date.now;
12
+ const refillPerMs = opts.perMinute / 60_000;
13
+ const buckets = new Map();
14
+ return {
15
+ consume(key) {
16
+ const t = now();
17
+ const b = buckets.get(key) ?? { tokens: opts.capacity, at: t };
18
+ b.tokens = Math.min(opts.capacity, b.tokens + (t - b.at) * refillPerMs);
19
+ b.at = t;
20
+ buckets.set(key, b);
21
+ if (b.tokens >= 1) {
22
+ b.tokens -= 1;
23
+ return { ok: true };
24
+ }
25
+ return { ok: false, retryAfterMs: Math.ceil((1 - b.tokens) / refillPerMs) };
26
+ },
27
+ };
28
+ }
29
+ function positiveInt(env, name, fallback) {
30
+ const raw = env[name];
31
+ if (raw === undefined || raw === "")
32
+ return fallback;
33
+ const n = Number(raw);
34
+ if (!Number.isInteger(n) || n <= 0)
35
+ throw new Error(`${name} must be a positive integer (got a value that is not)`);
36
+ return n;
37
+ }
38
+ /** The limits read from the environment. */
39
+ export function loadToolRateLimit(env = process.env) {
40
+ return {
41
+ capacity: positiveInt(env, "TOOL_RATE_LIMIT_BURST", 30),
42
+ perMinute: positiveInt(env, "TOOL_RATE_LIMIT_PER_MINUTE", 60),
43
+ };
44
+ }
45
+ /** The process-wide limiter every tool invocation goes through, unless a test injects its own. */
46
+ export const toolRateLimiter = createTokenBucketLimiter(loadToolRateLimit());
47
+ /** The stable machine code of a refused invocation (beside a message localized per X-MCP-Locale). */
48
+ export const RATE_LIMITED_CODE = "rate_limited";
@@ -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
+ }
@@ -26,8 +26,16 @@ export interface ServerManifest {
26
26
  features: Record<string, boolean>;
27
27
  /** The per-server context ceiling. A number cannot ride in `features` (booleans only). */
28
28
  contextCeiling: ContextCeiling;
29
+ /**
30
+ * The hosts the server is distributed on (mcp-server-standard S-45 / S-46), read from `distribution.targets`.
31
+ * Absent in the manifest: no target, `[]`. The ids are not validated here: the closed set is the standard's,
32
+ * checked by mcp-doctor; a fork adds a host by data, never by code.
33
+ */
34
+ distribution: {
35
+ targets: readonly string[];
36
+ };
29
37
  }
30
- type Expectation = "string" | "semver" | "object" | "boolean" | "positiveInteger" | "atLeastPerTool";
38
+ type Expectation = "string" | "semver" | "object" | "boolean" | "positiveInteger" | "atLeastPerTool" | "stringArray";
31
39
  /** Thrown when a manifest field cannot be read. `field` is the dotted path of the offender. */
32
40
  export declare class ServerManifestError extends Error {
33
41
  readonly field: string;
@@ -35,7 +35,7 @@ function isPositiveInteger(value) {
35
35
  export function parseServerManifest(document, source) {
36
36
  if (!isRecord(document))
37
37
  throw new ServerManifestError("(document)", source, "object");
38
- const { name, version, capabilities, transport, features, contextCeiling } = document;
38
+ const { name, version, capabilities, transport, features, contextCeiling, distribution } = document;
39
39
  if (typeof name !== "string" || name.length === 0) {
40
40
  throw new ServerManifestError("name", source, "string");
41
41
  }
@@ -65,6 +65,14 @@ export function parseServerManifest(document, source) {
65
65
  if (global < perTool) {
66
66
  throw new ServerManifestError("contextCeiling.global", source, "atLeastPerTool");
67
67
  }
68
+ let targets = [];
69
+ if (distribution !== undefined) {
70
+ const raw = isRecord(distribution) ? distribution.targets : undefined;
71
+ if (!Array.isArray(raw) || raw.some((t) => typeof t !== "string" || t.length === 0)) {
72
+ throw new ServerManifestError("distribution.targets", source, "stringArray");
73
+ }
74
+ targets = raw;
75
+ }
68
76
  return {
69
77
  name,
70
78
  version,
@@ -72,6 +80,7 @@ export function parseServerManifest(document, source) {
72
80
  transport,
73
81
  features: features,
74
82
  contextCeiling: { global, perTool },
83
+ distribution: { targets },
75
84
  };
76
85
  }
77
86
  /**
@@ -56,7 +56,11 @@ export function createCursorSigner(options = {}) {
56
56
  }
57
57
  // A copy: the caller zeroing or reusing its buffer must not change what this signer accepts.
58
58
  const key = supplied === undefined ? randomBytes(CURSOR_KEY_BYTES) : Buffer.from(supplied);
59
- const tagOf = (payloadPart) => createHmac("sha256", key).update(payloadPart).digest();
59
+ const tagOf = (payloadPart) =>
60
+ // allow-local-identity: HMAC proving a pagination cursor was issued by this server; it identifies no caller or tenant
61
+ createHmac("sha256", key)
62
+ .update(payloadPart)
63
+ .digest();
60
64
  const sign = (payload) => {
61
65
  // A cursor always says something; an empty one would encode to an empty part, which `verify`
62
66
  // refuses. Refused here, loudly, rather than issued and then rejected.
@@ -89,6 +93,7 @@ export function createCursorSigner(options = {}) {
89
93
  // the tag matches. Equal lengths (32 bytes each, checked above): timingSafeEqual cannot throw.
90
94
  if (!BASE64URL.test(payloadPart))
91
95
  throw new InvalidCursorError("not_base64url");
96
+ // allow-local-identity: constant-time check of the cursor's own HMAC tag; no credential a caller presented
92
97
  if (!timingSafeEqual(tagOf(payloadPart), tag))
93
98
  throw new InvalidCursorError("tag_mismatch");
94
99
  // Signed by us, so canonical; re-checked so a decode is never lenient.